Skip to content

Commit a9a21f3

Browse files
Extract 'printLocation' & 'printSourceLocation' functions (#1984)
Context: facebook/relay#2752 (comment)
1 parent 3ddf148 commit a9a21f3

File tree

8 files changed

+157
-148
lines changed

8 files changed

+157
-148
lines changed

src/error/GraphQLError.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// @flow strict
22

33
import isObjectLike from '../jsutils/isObjectLike';
4-
import { printError } from './printError';
54
import { type ASTNode } from '../language/ast';
65
import { type Source } from '../language/source';
76
import { type SourceLocation, getLocation } from '../language/location';
7+
import { printLocation, printSourceLocation } from '../language/printLocation';
88

99
/**
1010
* A GraphQLError describes an Error found during the parse, validate, or
@@ -217,3 +217,25 @@ export function GraphQLError( // eslint-disable-line no-redeclare
217217
},
218218
},
219219
});
220+
221+
/**
222+
* Prints a GraphQLError to a string, representing useful location information
223+
* about the error's position in the source.
224+
*/
225+
export function printError(error: GraphQLError): string {
226+
let output = error.message;
227+
228+
if (error.nodes) {
229+
for (const node of error.nodes) {
230+
if (node.loc) {
231+
output += '\n\n' + printLocation(node.loc);
232+
}
233+
}
234+
} else if (error.source && error.locations) {
235+
for (const location of error.locations) {
236+
output += '\n\n' + printSourceLocation(error.source, location);
237+
}
238+
}
239+
240+
return output;
241+
}

src/error/__tests__/GraphQLError-test.js

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ import { describe, it } from 'mocha';
55

66
import dedent from '../../jsutils/dedent';
77
import invariant from '../../jsutils/invariant';
8-
import { Kind, parse, Source, GraphQLError, formatError } from '../../';
8+
import {
9+
Kind,
10+
parse,
11+
Source,
12+
GraphQLError,
13+
printError,
14+
formatError,
15+
} from '../../';
916

1017
const source = new Source(dedent`
1118
{
@@ -155,3 +162,71 @@ describe('GraphQLError', () => {
155162
});
156163
});
157164
});
165+
166+
describe('printError', () => {
167+
it('prints an error without location', () => {
168+
const error = new GraphQLError('Error without location');
169+
expect(printError(error)).to.equal('Error without location');
170+
});
171+
172+
it('prints an error using node without location', () => {
173+
const error = new GraphQLError(
174+
'Error attached to node without location',
175+
parse('{ foo }', { noLocation: true }),
176+
);
177+
expect(printError(error)).to.equal(
178+
'Error attached to node without location',
179+
);
180+
});
181+
182+
it('prints an error with nodes from different sources', () => {
183+
const docA = parse(
184+
new Source(
185+
dedent`
186+
type Foo {
187+
field: String
188+
}
189+
`,
190+
'SourceA',
191+
),
192+
);
193+
const opA = docA.definitions[0];
194+
invariant(opA && opA.kind === Kind.OBJECT_TYPE_DEFINITION && opA.fields);
195+
const fieldA = opA.fields[0];
196+
197+
const docB = parse(
198+
new Source(
199+
dedent`
200+
type Foo {
201+
field: Int
202+
}
203+
`,
204+
'SourceB',
205+
),
206+
);
207+
const opB = docB.definitions[0];
208+
invariant(opB && opB.kind === Kind.OBJECT_TYPE_DEFINITION && opB.fields);
209+
const fieldB = opB.fields[0];
210+
211+
const error = new GraphQLError('Example error with two nodes', [
212+
fieldA.type,
213+
fieldB.type,
214+
]);
215+
216+
expect(printError(error) + '\n').to.equal(dedent`
217+
Example error with two nodes
218+
219+
SourceA:2:10
220+
1: type Foo {
221+
2: field: String
222+
^
223+
3: }
224+
225+
SourceB:2:10
226+
1: type Foo {
227+
2: field: Int
228+
^
229+
3: }
230+
`);
231+
});
232+
});

src/error/__tests__/printError-test.js

Lines changed: 0 additions & 109 deletions
This file was deleted.

src/error/index.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
// @flow strict
22

3-
export { GraphQLError } from './GraphQLError';
3+
export { GraphQLError, printError } from './GraphQLError';
44

55
export { syntaxError } from './syntaxError';
66

77
export { locatedError } from './locatedError';
88

9-
export { printError } from './printError';
10-
119
export { formatError } from './formatError';
1210
export type { GraphQLFormattedError } from './formatError';

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ export type {
174174
export {
175175
Source,
176176
getLocation,
177+
// Print source location
178+
printLocation,
179+
printSourceLocation,
177180
// Lex
178181
createLexer,
179182
TokenKind,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// @flow strict
2+
3+
import { expect } from 'chai';
4+
import { describe, it } from 'mocha';
5+
6+
import dedent from '../../jsutils/dedent';
7+
import { Source } from '../../language';
8+
import { printSourceLocation } from '../printLocation';
9+
10+
describe('printLocation', () => {
11+
it('prints single digit line number with no padding', () => {
12+
const result = printSourceLocation(
13+
new Source('*', 'Test', { line: 9, column: 1 }),
14+
{ line: 1, column: 1 },
15+
);
16+
17+
expect(result + '\n').to.equal(dedent`
18+
Test:9:1
19+
9: *
20+
^
21+
`);
22+
});
23+
24+
it('prints an line numbers with correct padding', () => {
25+
const result = printSourceLocation(
26+
new Source('*\n', 'Test', { line: 9, column: 1 }),
27+
{ line: 1, column: 1 },
28+
);
29+
30+
expect(result + '\n').to.equal(dedent`
31+
Test:9:1
32+
9: *
33+
^
34+
10:
35+
`);
36+
});
37+
});

src/language/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export { Source } from './source';
55
export { getLocation } from './location';
66
export type { SourceLocation } from './location';
77

8+
export { printLocation, printSourceLocation } from './printLocation';
9+
810
export { Kind } from './kinds';
911
export type { KindEnum } from './kinds';
1012

src/error/printError.js renamed to src/language/printLocation.js

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,35 @@
11
// @flow strict
22

3-
import { type SourceLocation, getLocation } from '../language/location';
43
import { type Source } from '../language/source';
5-
import { type GraphQLError } from './GraphQLError';
4+
import { type Location } from '../language/ast';
5+
import { type SourceLocation, getLocation } from '../language/location';
66

77
/**
8-
* Prints a GraphQLError to a string, representing useful location information
9-
* about the error's position in the source.
8+
* Render a helpful description of the location in the GraphQL Source document.
109
*/
11-
export function printError(error: GraphQLError): string {
12-
const printedLocations = [];
13-
if (error.nodes) {
14-
for (const node of error.nodes) {
15-
if (node.loc) {
16-
printedLocations.push(
17-
highlightSourceAtLocation(
18-
node.loc.source,
19-
getLocation(node.loc.source, node.loc.start),
20-
),
21-
);
22-
}
23-
}
24-
} else if (error.source && error.locations) {
25-
const source = error.source;
26-
for (const location of error.locations) {
27-
printedLocations.push(highlightSourceAtLocation(source, location));
28-
}
29-
}
30-
return printedLocations.length === 0
31-
? error.message
32-
: [error.message, ...printedLocations].join('\n\n');
10+
export function printLocation(location: Location): string {
11+
return printSourceLocation(
12+
location.source,
13+
getLocation(location.source, location.start),
14+
);
3315
}
3416

3517
/**
36-
* Render a helpful description of the location of the error in the GraphQL
37-
* Source document.
18+
* Render a helpful description of the location in the GraphQL Source document.
3819
*/
39-
function highlightSourceAtLocation(
20+
export function printSourceLocation(
4021
source: Source,
41-
location: SourceLocation,
22+
sourceLocation: SourceLocation,
4223
): string {
4324
const firstLineColumnOffset = source.locationOffset.column - 1;
4425
const body = whitespace(firstLineColumnOffset) + source.body;
4526

46-
const lineIndex = location.line - 1;
27+
const lineIndex = sourceLocation.line - 1;
4728
const lineOffset = source.locationOffset.line - 1;
48-
const lineNum = location.line + lineOffset;
29+
const lineNum = sourceLocation.line + lineOffset;
4930

50-
const columnOffset = location.line === 1 ? firstLineColumnOffset : 0;
51-
const columnNum = location.column + columnOffset;
31+
const columnOffset = sourceLocation.line === 1 ? firstLineColumnOffset : 0;
32+
const columnNum = sourceLocation.column + columnOffset;
5233

5334
const lines = body.split(/\r\n|[\n\r]/g);
5435
return (

0 commit comments

Comments
 (0)