Skip to content

Commit e319bdf

Browse files
authored
Merge pull request #949 from dwwoelfel/error-context
Account for query's offset in file for errors
2 parents dc6ab96 + 501f2c0 commit e319bdf

File tree

3 files changed

+74
-12
lines changed

3 files changed

+74
-12
lines changed

src/error/syntaxError.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ export function syntaxError(
2222
description: string
2323
): GraphQLError {
2424
const location = getLocation(source, position);
25+
const line = location.line + source.locationOffset.line - 1;
26+
const column = location.column + source.locationOffset.column - 1;
2527
const error = new GraphQLError(
26-
`Syntax Error ${source.name} (${location.line}:${location.column}) ` +
27-
description + '\n\n' + highlightSourceAtLocation(source, location),
28+
`Syntax Error ${source.name} (${line}:${column}) ${description}` +
29+
'\n\n' + highlightSourceAtLocation(source, location),
2830
undefined,
2931
source,
3032
[ position ]
@@ -38,21 +40,29 @@ export function syntaxError(
3840
*/
3941
function highlightSourceAtLocation(source, location) {
4042
const line = location.line;
41-
const prevLineNum = (line - 1).toString();
42-
const lineNum = line.toString();
43-
const nextLineNum = (line + 1).toString();
43+
const lineOffset = source.locationOffset.line - 1;
44+
const columnOffset = source.locationOffset.column - 1;
45+
const contextLine = line + lineOffset;
46+
const prevLineNum = (contextLine - 1).toString();
47+
const lineNum = contextLine.toString();
48+
const nextLineNum = (contextLine + 1).toString();
4449
const padLen = nextLineNum.length;
4550
const lines = source.body.split(/\r\n|[\n\r]/g);
51+
lines[0] = whitespace(columnOffset) + lines[0];
4652
return (
4753
(line >= 2 ?
4854
lpad(padLen, prevLineNum) + ': ' + lines[line - 2] + '\n' : '') +
4955
lpad(padLen, lineNum) + ': ' + lines[line - 1] + '\n' +
50-
Array(2 + padLen + location.column).join(' ') + '^\n' +
56+
whitespace(2 + padLen + location.column - 1 + columnOffset) + '^\n' +
5157
(line < lines.length ?
5258
lpad(padLen, nextLineNum) + ': ' + lines[line] + '\n' : '')
5359
);
5460
}
5561

62+
function whitespace(len) {
63+
return Array(len + 1).join(' ');
64+
}
65+
5666
function lpad(len, str) {
57-
return Array(len - str.length + 1).join(' ') + str;
67+
return whitespace(len - str.length) + str;
5868
}

src/language/__tests__/lexer-test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,39 @@ describe('Lexer', () => {
120120

121121
});
122122

123+
it('updates line numbers in error for file context', () => {
124+
expect(() => {
125+
const str = '' +
126+
'\n' +
127+
'\n' +
128+
' ?\n' +
129+
'\n';
130+
const source = new Source(str, 'foo.js', { line: 11, column: 1 });
131+
return createLexer(source).advance();
132+
}).to.throw(
133+
'Syntax Error foo.js (13:6) ' +
134+
'Cannot parse the unexpected character "?".\n' +
135+
'\n' +
136+
'12: \n' +
137+
'13: ?\n' +
138+
' ^\n' +
139+
'14: \n'
140+
);
141+
});
142+
143+
it('updates column numbers in error for file context', () => {
144+
expect(() => {
145+
const source = new Source('?', 'foo.js', { line: 1, column: 5 });
146+
return createLexer(source).advance();
147+
}).to.throw(
148+
'Syntax Error foo.js (1:5) ' +
149+
'Cannot parse the unexpected character "?".\n' +
150+
'\n' +
151+
'1: ?\n' +
152+
' ^\n'
153+
);
154+
});
155+
123156
it('lexes strings', () => {
124157

125158
expect(

src/language/source.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,37 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11+
import invariant from '../jsutils/invariant';
12+
13+
type Location = {
14+
line: number,
15+
column: number,
16+
};
17+
1118
/**
12-
* A representation of source input to GraphQL. The name is optional,
13-
* but is mostly useful for clients who store GraphQL documents in
14-
* source files; for example, if the GraphQL input is in a file Foo.graphql,
15-
* it might be useful for name to be "Foo.graphql".
19+
* A representation of source input to GraphQL.
20+
* `name` and `locationOffset` are optional. They are useful for clients who
21+
* store GraphQL documents in source files; for example, if the GraphQL input
22+
* starts at line 40 in a file named Foo.graphql, it might be useful for name to
23+
* be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
24+
* line and column in locationOffset are 1-indexed
1625
*/
1726
export class Source {
1827
body: string;
1928
name: string;
29+
locationOffset: Location;
2030

21-
constructor(body: string, name?: string): void {
31+
constructor(body: string, name?: string, locationOffset?: Location): void {
2232
this.body = body;
2333
this.name = name || 'GraphQL request';
34+
this.locationOffset = locationOffset || { line: 1, column: 1 };
35+
invariant(
36+
this.locationOffset.line > 0,
37+
'line in locationOffset is 1-indexed and must be positive'
38+
);
39+
invariant(
40+
this.locationOffset.column > 0,
41+
'column in locationOffset is 1-indexed and must be positive'
42+
);
2443
}
2544
}

0 commit comments

Comments
 (0)