Skip to content

Commit

Permalink
New: printError() (#1129)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
leebyron authored Dec 8, 2017
1 parent 96f92f3 commit 5fec485
Show file tree
Hide file tree
Showing 12 changed files with 383 additions and 251 deletions.
14 changes: 8 additions & 6 deletions src/error/GraphQLError.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,12 @@
* @flow
*/

import { printError } from './printError';
import { getLocation } from '../language/location';
import type { SourceLocation } from '../language/location';
import type { ASTNode } from '../language/ast';
import type { Source } from '../language/source';

export type GraphQLErrorLocation = {|
+line: number,
+column: number,
|};

/**
* A GraphQLError describes an Error found during the parse, validate, or
* execute phases of performing a GraphQL operation. In addition to a message
Expand Down Expand Up @@ -52,7 +49,7 @@ declare class GraphQLError extends Error {
*
* Enumerable, and appears in the result of JSON.stringify().
*/
+locations: $ReadOnlyArray<GraphQLErrorLocation> | void;
+locations: $ReadOnlyArray<SourceLocation> | void;

/**
* An array describing the JSON-path into the execution response which
Expand Down Expand Up @@ -194,4 +191,9 @@ export function GraphQLError( // eslint-disable-line no-redeclare
(GraphQLError: any).prototype = Object.create(Error.prototype, {
constructor: { value: GraphQLError },
name: { value: 'GraphQLError' },
toString: {
value: function toString() {
return printError(this);
},
},
});
5 changes: 3 additions & 2 deletions src/error/formatError.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
*/

import invariant from '../jsutils/invariant';
import type { GraphQLError, GraphQLErrorLocation } from './GraphQLError';
import type { GraphQLError } from './GraphQLError';
import type { SourceLocation } from '../language/location';

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

export type GraphQLFormattedError = {
+message: string,
+locations: $ReadOnlyArray<GraphQLErrorLocation> | void,
+locations: $ReadOnlyArray<SourceLocation> | void,
+path: $ReadOnlyArray<string | number> | void,
// Extensions
+[key: string]: mixed,
Expand Down
2 changes: 1 addition & 1 deletion src/error/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
export { GraphQLError } from './GraphQLError';
export { syntaxError } from './syntaxError';
export { locatedError } from './locatedError';
export { printError } from './printError';
export { formatError } from './formatError';

export type { GraphQLErrorLocation } from './GraphQLError';
export type { GraphQLFormattedError } from './formatError';
76 changes: 76 additions & 0 deletions src/error/printError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type { SourceLocation } from '../language/location';
import type { Source } from '../language/source';
import type { GraphQLError } from './GraphQLError';

/**
* Prints a GraphQLError to a string, representing useful location information
* about the error's position in the source.
*/
export function printError(error: GraphQLError): string {
const source = error.source;
const locations = error.locations || [];
const printedLocations = locations.map(
location =>
source
? highlightSourceAtLocation(source, location)
: ` (${location.line}:${location.column})`,
);
return error.message + printedLocations.join('');
}

/**
* Render a helpful description of the location of the error in the GraphQL
* Source document.
*/
function highlightSourceAtLocation(
source: Source,
location: SourceLocation,
): string {
const line = location.line;
const lineOffset = source.locationOffset.line - 1;
const columnOffset = getColumnOffset(source, location);
const contextLine = line + lineOffset;
const contextColumn = location.column + columnOffset;
const prevLineNum = (contextLine - 1).toString();
const lineNum = contextLine.toString();
const nextLineNum = (contextLine + 1).toString();
const padLen = nextLineNum.length;
const lines = source.body.split(/\r\n|[\n\r]/g);
lines[0] = whitespace(source.locationOffset.column - 1) + lines[0];
return (
`\n\n${source.name} (${contextLine}:${contextColumn})\n` +
(line >= 2
? lpad(padLen, prevLineNum) + ': ' + lines[line - 2] + '\n'
: '') +
lpad(padLen, lineNum) +
': ' +
lines[line - 1] +
'\n' +
whitespace(2 + padLen + contextColumn - 1) +
'^\n' +
(line < lines.length
? lpad(padLen, nextLineNum) + ': ' + lines[line] + '\n'
: '')
);
}

function getColumnOffset(source: Source, location: SourceLocation): number {
return location.line === 1 ? source.locationOffset.column - 1 : 0;
}

function whitespace(len: number): string {
return Array(len + 1).join(' ');
}

function lpad(len: number, str: string): string {
return whitespace(len - str.length) + str;
}
62 changes: 3 additions & 59 deletions src/error/syntaxError.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
* @flow
*/

import { getLocation } from '../language/location';
import type { Source } from '../language/source';
import { GraphQLError } from './GraphQLError';

import type { SourceLocation } from '../language/location';

/**
* Produces a GraphQLError representing a syntax error, containing useful
* descriptive information about the syntax error's position in the source.
Expand All @@ -22,60 +19,7 @@ export function syntaxError(
position: number,
description: string,
): GraphQLError {
const location = getLocation(source, position);
const line = location.line + source.locationOffset.line - 1;
const columnOffset = getColumnOffset(source, location);
const column = location.column + columnOffset;
const error = new GraphQLError(
`Syntax Error ${source.name} (${line}:${column}) ${description}` +
'\n\n' +
highlightSourceAtLocation(source, location),
undefined,
source,
[position],
);
return error;
}

/**
* Render a helpful description of the location of the error in the GraphQL
* Source document.
*/
function highlightSourceAtLocation(source, location) {
const line = location.line;
const lineOffset = source.locationOffset.line - 1;
const columnOffset = getColumnOffset(source, location);
const contextLine = line + lineOffset;
const prevLineNum = (contextLine - 1).toString();
const lineNum = contextLine.toString();
const nextLineNum = (contextLine + 1).toString();
const padLen = nextLineNum.length;
const lines = source.body.split(/\r\n|[\n\r]/g);
lines[0] = whitespace(source.locationOffset.column - 1) + lines[0];
return (
(line >= 2
? lpad(padLen, prevLineNum) + ': ' + lines[line - 2] + '\n'
: '') +
lpad(padLen, lineNum) +
': ' +
lines[line - 1] +
'\n' +
whitespace(2 + padLen + location.column - 1 + columnOffset) +
'^\n' +
(line < lines.length
? lpad(padLen, nextLineNum) + ': ' + lines[line] + '\n'
: '')
);
}

function getColumnOffset(source: Source, location: SourceLocation): number {
return location.line === 1 ? source.locationOffset.column - 1 : 0;
}

function whitespace(len) {
return Array(len + 1).join(' ');
}

function lpad(len, str) {
return whitespace(len - str.length) + str;
return new GraphQLError(`Syntax Error: ${description}`, undefined, source, [
position,
]);
}
5 changes: 1 addition & 4 deletions src/execution/__tests__/sync-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,7 @@ describe('Execute: synchronously when possible', () => {
expect(result).to.containSubset({
errors: [
{
message:
'Syntax Error GraphQL request (1:29) Expected Name, found {\n\n' +
'1: fragment Example on Query { { { syncField }\n' +
' ^\n',
message: 'Syntax Error: Expected Name, found {',
locations: [{ line: 1, column: 29 }],
},
],
Expand Down
7 changes: 4 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export type {
// Parse and operate on GraphQL language source files.
export {
Source,
SourceLocation,
getLocation,
// Parse
parse,
Expand Down Expand Up @@ -265,10 +266,10 @@ export {
VariablesInAllowedPositionRule,
} from './validation';

// Create and format GraphQL errors.
export { GraphQLError, formatError } from './error';
// Create, format, and print GraphQL errors.
export { GraphQLError, formatError, printError } from './error';

export type { GraphQLFormattedError, GraphQLErrorLocation } from './error';
export type { GraphQLFormattedError } from './error';

// Utilities for operating on GraphQL type schema and parsed sources.
export {
Expand Down
Loading

0 comments on commit 5fec485

Please sign in to comment.