Skip to content

Commit b374965

Browse files
authored
Fix printError/locations for multiple nodes. (#1131)
If a GraphQLError represents multiple nodes across files (could happen for validation across multiple parsed files) then the reported locations and printError output can be incorrect for the second node. This ensures locations are derived from nodes whenever possible to get correct location and amends comment documentation.
1 parent f6e781d commit b374965

File tree

3 files changed

+104
-28
lines changed

3 files changed

+104
-28
lines changed

src/error/GraphQLError.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ declare class GraphQLError extends Error {
6565
+nodes: $ReadOnlyArray<ASTNode> | void;
6666

6767
/**
68-
* The source GraphQL document corresponding to this error.
68+
* The source GraphQL document for the first location of this error.
69+
*
70+
* Note that if this Error represents more than one node, the source may not
71+
* represent nodes after the first node.
6972
*/
7073
+source: Source | void;
7174

@@ -121,9 +124,15 @@ export function GraphQLError( // eslint-disable-line no-redeclare
121124
}
122125

123126
let _locations;
124-
const _source2 = _source; // seems here Flow need a const to resolve type.
125-
if (_source2 && _positions) {
126-
_locations = _positions.map(pos => getLocation(_source2, pos));
127+
if (positions && source) {
128+
_locations = positions.map(pos => getLocation(source, pos));
129+
} else if (_nodes) {
130+
_locations = _nodes.reduce((list, node) => {
131+
if (node.loc) {
132+
list.push(getLocation(node.loc.source, node.loc.start));
133+
}
134+
return list;
135+
}, []);
127136
}
128137

129138
Object.defineProperties(this, {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
8+
import { expect } from 'chai';
9+
import { describe, it } from 'mocha';
10+
11+
import { GraphQLError } from '../GraphQLError';
12+
import { printError } from '../printError';
13+
import { parse, Source } from '../../language';
14+
import dedent from '../../jsutils/dedent';
15+
16+
describe('printError', () => {
17+
it('prints an error with nodes from different sources', () => {
18+
const sourceA = parse(
19+
new Source(
20+
dedent`
21+
type Foo {
22+
field: String
23+
}`,
24+
'SourceA',
25+
),
26+
);
27+
const fieldTypeA = sourceA.definitions[0].fields[0].type;
28+
29+
const sourceB = parse(
30+
new Source(
31+
dedent`
32+
type Foo {
33+
field: Int
34+
}`,
35+
'SourceB',
36+
),
37+
);
38+
const fieldTypeB = sourceB.definitions[0].fields[0].type;
39+
40+
const error = new GraphQLError('Example error with two nodes', [
41+
fieldTypeA,
42+
fieldTypeB,
43+
]);
44+
45+
expect(printError(error)).to.equal(dedent`
46+
Example error with two nodes
47+
48+
SourceA (2:10)
49+
1: type Foo {
50+
2: field: String
51+
^
52+
3: }
53+
54+
SourceB (2:10)
55+
1: type Foo {
56+
2: field: Int
57+
^
58+
3: }
59+
`);
60+
});
61+
});

src/error/printError.js

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
import { getLocation } from '../language/location';
1011
import type { SourceLocation } from '../language/location';
1112
import type { Source } from '../language/source';
1213
import type { GraphQLError } from './GraphQLError';
@@ -16,15 +17,27 @@ import type { GraphQLError } from './GraphQLError';
1617
* about the error's position in the source.
1718
*/
1819
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('');
20+
const printedLocations = [];
21+
if (error.nodes) {
22+
error.nodes.forEach(node => {
23+
if (node.loc) {
24+
printedLocations.push(
25+
highlightSourceAtLocation(
26+
node.loc.source,
27+
getLocation(node.loc.source, node.loc.start),
28+
),
29+
);
30+
}
31+
});
32+
} else if (error.source && error.locations) {
33+
const source = error.source;
34+
error.locations.forEach(location => {
35+
printedLocations.push(highlightSourceAtLocation(source, location));
36+
});
37+
}
38+
return printedLocations.length === 0
39+
? error.message
40+
: [error.message, ...printedLocations].join('\n\n') + '\n';
2841
}
2942

3043
/**
@@ -46,21 +59,14 @@ function highlightSourceAtLocation(
4659
const padLen = nextLineNum.length;
4760
const lines = source.body.split(/\r\n|[\n\r]/g);
4861
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-
);
62+
const outputLines = [
63+
`${source.name} (${contextLine}:${contextColumn})`,
64+
line >= 2 && lpad(padLen, prevLineNum) + ': ' + lines[line - 2],
65+
lpad(padLen, lineNum) + ': ' + lines[line - 1],
66+
whitespace(2 + padLen + contextColumn - 1) + '^',
67+
line < lines.length && lpad(padLen, nextLineNum) + ': ' + lines[line],
68+
];
69+
return outputLines.filter(Boolean).join('\n');
6470
}
6571

6672
function getColumnOffset(source: Source, location: SourceLocation): number {

0 commit comments

Comments
 (0)