Skip to content

Commit 5fec485

Browse files
authored
New: printError() (#1129)
Lifted from / inspired by a similar change in #722, this creates a new function `printError()` (and uses it as the implementation for `GraphQLError#toString()`) which prints location information in the context of an error. This is moved from the syntax error where it used to be hard-coded, so it may now be used to format validation errors, value coercion errors, or any other error which may be associated with a location.
1 parent 96f92f3 commit 5fec485

File tree

12 files changed

+383
-251
lines changed

12 files changed

+383
-251
lines changed

src/error/GraphQLError.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@
77
* @flow
88
*/
99

10+
import { printError } from './printError';
1011
import { getLocation } from '../language/location';
12+
import type { SourceLocation } from '../language/location';
1113
import type { ASTNode } from '../language/ast';
1214
import type { Source } from '../language/source';
1315

14-
export type GraphQLErrorLocation = {|
15-
+line: number,
16-
+column: number,
17-
|};
18-
1916
/**
2017
* A GraphQLError describes an Error found during the parse, validate, or
2118
* execute phases of performing a GraphQL operation. In addition to a message
@@ -52,7 +49,7 @@ declare class GraphQLError extends Error {
5249
*
5350
* Enumerable, and appears in the result of JSON.stringify().
5451
*/
55-
+locations: $ReadOnlyArray<GraphQLErrorLocation> | void;
52+
+locations: $ReadOnlyArray<SourceLocation> | void;
5653

5754
/**
5855
* An array describing the JSON-path into the execution response which
@@ -194,4 +191,9 @@ export function GraphQLError( // eslint-disable-line no-redeclare
194191
(GraphQLError: any).prototype = Object.create(Error.prototype, {
195192
constructor: { value: GraphQLError },
196193
name: { value: 'GraphQLError' },
194+
toString: {
195+
value: function toString() {
196+
return printError(this);
197+
},
198+
},
197199
});

src/error/formatError.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
*/
99

1010
import invariant from '../jsutils/invariant';
11-
import type { GraphQLError, GraphQLErrorLocation } from './GraphQLError';
11+
import type { GraphQLError } from './GraphQLError';
12+
import type { SourceLocation } from '../language/location';
1213

1314
/**
1415
* Given a GraphQLError, format it according to the rules described by the
@@ -26,7 +27,7 @@ export function formatError(error: GraphQLError): GraphQLFormattedError {
2627

2728
export type GraphQLFormattedError = {
2829
+message: string,
29-
+locations: $ReadOnlyArray<GraphQLErrorLocation> | void,
30+
+locations: $ReadOnlyArray<SourceLocation> | void,
3031
+path: $ReadOnlyArray<string | number> | void,
3132
// Extensions
3233
+[key: string]: mixed,

src/error/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
export { GraphQLError } from './GraphQLError';
1111
export { syntaxError } from './syntaxError';
1212
export { locatedError } from './locatedError';
13+
export { printError } from './printError';
1314
export { formatError } from './formatError';
1415

15-
export type { GraphQLErrorLocation } from './GraphQLError';
1616
export type { GraphQLFormattedError } from './formatError';

src/error/printError.js

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type { SourceLocation } from '../language/location';
11+
import type { Source } from '../language/source';
12+
import type { GraphQLError } from './GraphQLError';
13+
14+
/**
15+
* Prints a GraphQLError to a string, representing useful location information
16+
* about the error's position in the source.
17+
*/
18+
export function printError(error: GraphQLError): string {
19+
const source = error.source;
20+
const locations = error.locations || [];
21+
const printedLocations = locations.map(
22+
location =>
23+
source
24+
? highlightSourceAtLocation(source, location)
25+
: ` (${location.line}:${location.column})`,
26+
);
27+
return error.message + printedLocations.join('');
28+
}
29+
30+
/**
31+
* Render a helpful description of the location of the error in the GraphQL
32+
* Source document.
33+
*/
34+
function highlightSourceAtLocation(
35+
source: Source,
36+
location: SourceLocation,
37+
): string {
38+
const line = location.line;
39+
const lineOffset = source.locationOffset.line - 1;
40+
const columnOffset = getColumnOffset(source, location);
41+
const contextLine = line + lineOffset;
42+
const contextColumn = location.column + columnOffset;
43+
const prevLineNum = (contextLine - 1).toString();
44+
const lineNum = contextLine.toString();
45+
const nextLineNum = (contextLine + 1).toString();
46+
const padLen = nextLineNum.length;
47+
const lines = source.body.split(/\r\n|[\n\r]/g);
48+
lines[0] = whitespace(source.locationOffset.column - 1) + lines[0];
49+
return (
50+
`\n\n${source.name} (${contextLine}:${contextColumn})\n` +
51+
(line >= 2
52+
? lpad(padLen, prevLineNum) + ': ' + lines[line - 2] + '\n'
53+
: '') +
54+
lpad(padLen, lineNum) +
55+
': ' +
56+
lines[line - 1] +
57+
'\n' +
58+
whitespace(2 + padLen + contextColumn - 1) +
59+
'^\n' +
60+
(line < lines.length
61+
? lpad(padLen, nextLineNum) + ': ' + lines[line] + '\n'
62+
: '')
63+
);
64+
}
65+
66+
function getColumnOffset(source: Source, location: SourceLocation): number {
67+
return location.line === 1 ? source.locationOffset.column - 1 : 0;
68+
}
69+
70+
function whitespace(len: number): string {
71+
return Array(len + 1).join(' ');
72+
}
73+
74+
function lpad(len: number, str: string): string {
75+
return whitespace(len - str.length) + str;
76+
}

src/error/syntaxError.js

+3-59
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@
77
* @flow
88
*/
99

10-
import { getLocation } from '../language/location';
1110
import type { Source } from '../language/source';
1211
import { GraphQLError } from './GraphQLError';
1312

14-
import type { SourceLocation } from '../language/location';
15-
1613
/**
1714
* Produces a GraphQLError representing a syntax error, containing useful
1815
* descriptive information about the syntax error's position in the source.
@@ -22,60 +19,7 @@ export function syntaxError(
2219
position: number,
2320
description: string,
2421
): GraphQLError {
25-
const location = getLocation(source, position);
26-
const line = location.line + source.locationOffset.line - 1;
27-
const columnOffset = getColumnOffset(source, location);
28-
const column = location.column + columnOffset;
29-
const error = new GraphQLError(
30-
`Syntax Error ${source.name} (${line}:${column}) ${description}` +
31-
'\n\n' +
32-
highlightSourceAtLocation(source, location),
33-
undefined,
34-
source,
35-
[position],
36-
);
37-
return error;
38-
}
39-
40-
/**
41-
* Render a helpful description of the location of the error in the GraphQL
42-
* Source document.
43-
*/
44-
function highlightSourceAtLocation(source, location) {
45-
const line = location.line;
46-
const lineOffset = source.locationOffset.line - 1;
47-
const columnOffset = getColumnOffset(source, location);
48-
const contextLine = line + lineOffset;
49-
const prevLineNum = (contextLine - 1).toString();
50-
const lineNum = contextLine.toString();
51-
const nextLineNum = (contextLine + 1).toString();
52-
const padLen = nextLineNum.length;
53-
const lines = source.body.split(/\r\n|[\n\r]/g);
54-
lines[0] = whitespace(source.locationOffset.column - 1) + lines[0];
55-
return (
56-
(line >= 2
57-
? lpad(padLen, prevLineNum) + ': ' + lines[line - 2] + '\n'
58-
: '') +
59-
lpad(padLen, lineNum) +
60-
': ' +
61-
lines[line - 1] +
62-
'\n' +
63-
whitespace(2 + padLen + location.column - 1 + columnOffset) +
64-
'^\n' +
65-
(line < lines.length
66-
? lpad(padLen, nextLineNum) + ': ' + lines[line] + '\n'
67-
: '')
68-
);
69-
}
70-
71-
function getColumnOffset(source: Source, location: SourceLocation): number {
72-
return location.line === 1 ? source.locationOffset.column - 1 : 0;
73-
}
74-
75-
function whitespace(len) {
76-
return Array(len + 1).join(' ');
77-
}
78-
79-
function lpad(len, str) {
80-
return whitespace(len - str.length) + str;
22+
return new GraphQLError(`Syntax Error: ${description}`, undefined, source, [
23+
position,
24+
]);
8125
}

src/execution/__tests__/sync-test.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,7 @@ describe('Execute: synchronously when possible', () => {
8484
expect(result).to.containSubset({
8585
errors: [
8686
{
87-
message:
88-
'Syntax Error GraphQL request (1:29) Expected Name, found {\n\n' +
89-
'1: fragment Example on Query { { { syncField }\n' +
90-
' ^\n',
87+
message: 'Syntax Error: Expected Name, found {',
9188
locations: [{ line: 1, column: 29 }],
9289
},
9390
],

src/index.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export type {
145145
// Parse and operate on GraphQL language source files.
146146
export {
147147
Source,
148+
SourceLocation,
148149
getLocation,
149150
// Parse
150151
parse,
@@ -265,10 +266,10 @@ export {
265266
VariablesInAllowedPositionRule,
266267
} from './validation';
267268

268-
// Create and format GraphQL errors.
269-
export { GraphQLError, formatError } from './error';
269+
// Create, format, and print GraphQL errors.
270+
export { GraphQLError, formatError, printError } from './error';
270271

271-
export type { GraphQLFormattedError, GraphQLErrorLocation } from './error';
272+
export type { GraphQLFormattedError } from './error';
272273

273274
// Utilities for operating on GraphQL type schema and parsed sources.
274275
export {

0 commit comments

Comments
 (0)