From 6fc2b7740f18f60ae110b09f2606c83d653e537b Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Thu, 7 Dec 2017 16:53:57 -0800 Subject: [PATCH] New: printError() 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. --- src/error/GraphQLError.js | 14 +- src/error/formatError.js | 5 +- src/error/index.js | 2 +- src/error/printError.js | 76 +++++ src/error/syntaxError.js | 62 +---- src/execution/__tests__/sync-test.js | 5 +- src/index.js | 7 +- src/language/__tests__/lexer-test.js | 277 +++++++++++-------- src/language/__tests__/parser-test.js | 87 ++++-- src/language/__tests__/schema-parser-test.js | 90 +++--- src/language/index.js | 1 + src/language/location.js | 8 +- 12 files changed, 383 insertions(+), 251 deletions(-) create mode 100644 src/error/printError.js diff --git a/src/error/GraphQLError.js b/src/error/GraphQLError.js index 77dbe486679..92e842a3fdf 100644 --- a/src/error/GraphQLError.js +++ b/src/error/GraphQLError.js @@ -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 @@ -52,7 +49,7 @@ declare class GraphQLError extends Error { * * Enumerable, and appears in the result of JSON.stringify(). */ - +locations: $ReadOnlyArray | void; + +locations: $ReadOnlyArray | void; /** * An array describing the JSON-path into the execution response which @@ -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); + }, + }, }); diff --git a/src/error/formatError.js b/src/error/formatError.js index 437c699cca5..f23f0cede73 100644 --- a/src/error/formatError.js +++ b/src/error/formatError.js @@ -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 @@ -26,7 +27,7 @@ export function formatError(error: GraphQLError): GraphQLFormattedError { export type GraphQLFormattedError = { +message: string, - +locations: $ReadOnlyArray | void, + +locations: $ReadOnlyArray | void, +path: $ReadOnlyArray | void, // Extensions +[key: string]: mixed, diff --git a/src/error/index.js b/src/error/index.js index b6726fe59d2..6a950d93916 100644 --- a/src/error/index.js +++ b/src/error/index.js @@ -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'; diff --git a/src/error/printError.js b/src/error/printError.js new file mode 100644 index 00000000000..36d78a86bc4 --- /dev/null +++ b/src/error/printError.js @@ -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 { 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; +} diff --git a/src/error/syntaxError.js b/src/error/syntaxError.js index 4f2965cd2d0..ac1ac2ca054 100644 --- a/src/error/syntaxError.js +++ b/src/error/syntaxError.js @@ -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. @@ -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, + ]); } diff --git a/src/execution/__tests__/sync-test.js b/src/execution/__tests__/sync-test.js index 0c5b4661070..57a94be661f 100644 --- a/src/execution/__tests__/sync-test.js +++ b/src/execution/__tests__/sync-test.js @@ -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 }], }, ], diff --git a/src/index.js b/src/index.js index 6073a3fc4a0..0ea928fbe6e 100644 --- a/src/index.js +++ b/src/index.js @@ -145,6 +145,7 @@ export type { // Parse and operate on GraphQL language source files. export { Source, + SourceLocation, getLocation, // Parse parse, @@ -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 { diff --git a/src/language/__tests__/lexer-test.js b/src/language/__tests__/lexer-test.js index af07eaa31a5..34af225f2b0 100644 --- a/src/language/__tests__/lexer-test.js +++ b/src/language/__tests__/lexer-test.js @@ -15,13 +15,24 @@ function lexOne(str) { return lexer.advance(); } +function expectSyntaxError(text, message, location) { + try { + lexOne(text); + expect.fail('Expected to throw syntax error'); + } catch (error) { + expect(error.message).to.contain(message); + expect(error.locations).to.deep.equal([location]); + } +} + /* eslint-disable max-len */ describe('Lexer', () => { it('disallows uncommon control characters', () => { - expect(() => lexOne('\u0007')).to.throw( - 'Syntax Error GraphQL request (1:1) ' + - 'Cannot contain the invalid character "\\u0007"', + expectSyntaxError( + '\u0007', + 'Cannot contain the invalid character "\\u0007"', + { line: 1, column: 1 }, ); }); @@ -94,17 +105,21 @@ describe('Lexer', () => { }); it('errors respect whitespace', () => { - expect(() => + let caughtError; + try { lexOne(` ? -`), - ).to.throw( - 'Syntax Error GraphQL request (3:5) ' + - 'Cannot parse the unexpected character "?".\n' + +`); + } catch (error) { + caughtError = error; + } + expect(String(caughtError)).to.equal( + 'Syntax Error: Cannot parse the unexpected character "?".\n' + '\n' + + 'GraphQL request (3:5)\n' + '2: \n' + '3: ?\n' + ' ^\n' + @@ -113,14 +128,18 @@ describe('Lexer', () => { }); it('updates line numbers in error for file context', () => { - expect(() => { + let caughtError; + try { const str = '' + '\n' + '\n' + ' ?\n' + '\n'; const source = new Source(str, 'foo.js', { line: 11, column: 12 }); - return createLexer(source).advance(); - }).to.throw( - 'Syntax Error foo.js (13:6) ' + - 'Cannot parse the unexpected character "?".\n' + + createLexer(source).advance(); + } catch (error) { + caughtError = error; + } + expect(String(caughtError)).to.equal( + 'Syntax Error: Cannot parse the unexpected character "?".\n' + '\n' + + 'foo.js (13:6)\n' + '12: \n' + '13: ?\n' + ' ^\n' + @@ -129,13 +148,17 @@ describe('Lexer', () => { }); it('updates column numbers in error for file context', () => { - expect(() => { + let caughtError; + try { const source = new Source('?', 'foo.js', { line: 1, column: 5 }); - return createLexer(source).advance(); - }).to.throw( - 'Syntax Error foo.js (1:5) ' + - 'Cannot parse the unexpected character "?".\n' + + createLexer(source).advance(); + } catch (error) { + caughtError = error; + } + expect(String(caughtError)).to.equal( + 'Syntax Error: Cannot parse the unexpected character "?".\n' + '\n' + + 'foo.js (1:5)\n' + '1: ?\n' + ' ^\n', ); @@ -186,61 +209,82 @@ describe('Lexer', () => { }); it('lex reports useful string errors', () => { - expect(() => lexOne('"')).to.throw( - 'Syntax Error GraphQL request (1:2) Unterminated string.', - ); + expectSyntaxError('"', 'Unterminated string.', { line: 1, column: 2 }); - expect(() => lexOne('"no end quote')).to.throw( - 'Syntax Error GraphQL request (1:14) Unterminated string.', - ); + expectSyntaxError('"no end quote', 'Unterminated string.', { + line: 1, + column: 14, + }); - expect(() => lexOne("'single quotes'")).to.throw( - "Syntax Error GraphQL request (1:1) Unexpected single quote character ('), " + + expectSyntaxError( + "'single quotes'", + "Unexpected single quote character ('), " + 'did you mean to use a double quote (")?', + { line: 1, column: 1 }, ); - expect(() => lexOne('"contains unescaped \u0007 control char"')).to.throw( - 'Syntax Error GraphQL request (1:21) Invalid character within String: "\\u0007".', + expectSyntaxError( + '"contains unescaped \u0007 control char"', + 'Invalid character within String: "\\u0007".', + { line: 1, column: 21 }, ); - expect(() => lexOne('"null-byte is not \u0000 end of file"')).to.throw( - 'Syntax Error GraphQL request (1:19) Invalid character within String: "\\u0000".', + expectSyntaxError( + '"null-byte is not \u0000 end of file"', + 'Invalid character within String: "\\u0000".', + { line: 1, column: 19 }, ); - expect(() => lexOne('"multi\nline"')).to.throw( - 'Syntax Error GraphQL request (1:7) Unterminated string', - ); + expectSyntaxError('"multi\nline"', 'Unterminated string', { + line: 1, + column: 7, + }); - expect(() => lexOne('"multi\rline"')).to.throw( - 'Syntax Error GraphQL request (1:7) Unterminated string', - ); + expectSyntaxError('"multi\rline"', 'Unterminated string', { + line: 1, + column: 7, + }); - expect(() => lexOne('"bad \\z esc"')).to.throw( - 'Syntax Error GraphQL request (1:7) Invalid character escape sequence: \\z.', + expectSyntaxError( + '"bad \\z esc"', + 'Invalid character escape sequence: \\z.', + { line: 1, column: 7 }, ); - expect(() => lexOne('"bad \\x esc"')).to.throw( - 'Syntax Error GraphQL request (1:7) Invalid character escape sequence: \\x.', + expectSyntaxError( + '"bad \\x esc"', + 'Invalid character escape sequence: \\x.', + { line: 1, column: 7 }, ); - expect(() => lexOne('"bad \\u1 esc"')).to.throw( - 'Syntax Error GraphQL request (1:7) Invalid character escape sequence: \\u1 es.', + expectSyntaxError( + '"bad \\u1 esc"', + 'Invalid character escape sequence: \\u1 es.', + { line: 1, column: 7 }, ); - expect(() => lexOne('"bad \\u0XX1 esc"')).to.throw( - 'Syntax Error GraphQL request (1:7) Invalid character escape sequence: \\u0XX1.', + expectSyntaxError( + '"bad \\u0XX1 esc"', + 'Invalid character escape sequence: \\u0XX1.', + { line: 1, column: 7 }, ); - expect(() => lexOne('"bad \\uXXXX esc"')).to.throw( - 'Syntax Error GraphQL request (1:7) Invalid character escape sequence: \\uXXXX.', + expectSyntaxError( + '"bad \\uXXXX esc"', + 'Invalid character escape sequence: \\uXXXX.', + { line: 1, column: 7 }, ); - expect(() => lexOne('"bad \\uFXXX esc"')).to.throw( - 'Syntax Error GraphQL request (1:7) Invalid character escape sequence: \\uFXXX.', + expectSyntaxError( + '"bad \\uFXXX esc"', + 'Invalid character escape sequence: \\uFXXX.', + { line: 1, column: 7 }, ); - expect(() => lexOne('"bad \\uXXXF esc"')).to.throw( - 'Syntax Error GraphQL request (1:7) Invalid character escape sequence: \\uXXXF.', + expectSyntaxError( + '"bad \\uXXXF esc"', + 'Invalid character escape sequence: \\uXXXF.', + { line: 1, column: 7 }, ); }); @@ -318,22 +362,23 @@ describe('Lexer', () => { }); it('lex reports useful block string errors', () => { - expect(() => lexOne('"""')).to.throw( - 'Syntax Error GraphQL request (1:4) Unterminated string.', - ); + expectSyntaxError('"""', 'Unterminated string.', { line: 1, column: 4 }); - expect(() => lexOne('"""no end quote')).to.throw( - 'Syntax Error GraphQL request (1:16) Unterminated string.', - ); + expectSyntaxError('"""no end quote', 'Unterminated string.', { + line: 1, + column: 16, + }); - expect(() => - lexOne('"""contains unescaped \u0007 control char"""'), - ).to.throw( - 'Syntax Error GraphQL request (1:23) Invalid character within String: "\\u0007".', + expectSyntaxError( + '"""contains unescaped \u0007 control char"""', + 'Invalid character within String: "\\u0007".', + { line: 1, column: 23 }, ); - expect(() => lexOne('"""null-byte is not \u0000 end of file"""')).to.throw( - 'Syntax Error GraphQL request (1:21) Invalid character within String: "\\u0000".', + expectSyntaxError( + '"""null-byte is not \u0000 end of file"""', + 'Invalid character within String: "\\u0000".', + { line: 1, column: 21 }, ); }); @@ -452,48 +497,51 @@ describe('Lexer', () => { }); it('lex reports useful number errors', () => { - expect(() => lexOne('00')).to.throw( - 'Syntax Error GraphQL request (1:2) Invalid number, ' + - 'unexpected digit after 0: "0".', - ); + expectSyntaxError('00', 'Invalid number, unexpected digit after 0: "0".', { + line: 1, + column: 2, + }); - expect(() => lexOne('+1')).to.throw( - 'Syntax Error GraphQL request (1:1) Cannot parse the unexpected character "+".', - ); + expectSyntaxError('+1', 'Cannot parse the unexpected character "+".', { + line: 1, + column: 1, + }); - expect(() => lexOne('1.')).to.throw( - 'Syntax Error GraphQL request (1:3) Invalid number, ' + - 'expected digit but got: .', - ); + expectSyntaxError('1.', 'Invalid number, expected digit but got: .', { + line: 1, + column: 3, + }); - expect(() => lexOne('1.e1')).to.throw( - 'Syntax Error GraphQL request (1:3) Invalid number, ' + - 'expected digit but got: "e".', - ); + expectSyntaxError('1.e1', 'Invalid number, expected digit but got: "e".', { + line: 1, + column: 3, + }); - expect(() => lexOne('.123')).to.throw( - 'Syntax Error GraphQL request (1:1) Cannot parse the unexpected character ".".', - ); + expectSyntaxError('.123', 'Cannot parse the unexpected character ".".', { + line: 1, + column: 1, + }); - expect(() => lexOne('1.A')).to.throw( - 'Syntax Error GraphQL request (1:3) Invalid number, ' + - 'expected digit but got: "A".', - ); + expectSyntaxError('1.A', 'Invalid number, expected digit but got: "A".', { + line: 1, + column: 3, + }); - expect(() => lexOne('-A')).to.throw( - 'Syntax Error GraphQL request (1:2) Invalid number, ' + - 'expected digit but got: "A".', - ); + expectSyntaxError('-A', 'Invalid number, expected digit but got: "A".', { + line: 1, + column: 2, + }); - expect(() => lexOne('1.0e')).to.throw( - 'Syntax Error GraphQL request (1:5) Invalid number, ' + - 'expected digit but got: .', + expectSyntaxError( + '1.0e', + 'Invalid number, expected digit but got: .', + { line: 1, column: 5 }, ); - expect(() => lexOne('1.0eA')).to.throw( - 'Syntax Error GraphQL request (1:5) Invalid number, ' + - 'expected digit but got: "A".', - ); + expectSyntaxError('1.0eA', 'Invalid number, expected digit but got: "A".', { + line: 1, + column: 5, + }); }); it('lexes punctuation', () => { @@ -590,22 +638,26 @@ describe('Lexer', () => { }); it('lex reports useful unknown character error', () => { - expect(() => lexOne('..')).to.throw( - 'Syntax Error GraphQL request (1:1) Cannot parse the unexpected character ".".', - ); + expectSyntaxError('..', 'Cannot parse the unexpected character ".".', { + line: 1, + column: 1, + }); - expect(() => lexOne('?')).to.throw( - 'Syntax Error GraphQL request (1:1) Cannot parse the unexpected character "?".', - ); + expectSyntaxError('?', 'Cannot parse the unexpected character "?".', { + line: 1, + column: 1, + }); - expect(() => lexOne('\u203B')).to.throw( - 'Syntax Error GraphQL request (1:1) ' + - 'Cannot parse the unexpected character "\\u203B".', + expectSyntaxError( + '\u203B', + 'Cannot parse the unexpected character "\\u203B".', + { line: 1, column: 1 }, ); - expect(() => lexOne('\u200b')).to.throw( - 'Syntax Error GraphQL request (1:1) ' + - 'Cannot parse the unexpected character "\\u200B".', + expectSyntaxError( + '\u200b', + 'Cannot parse the unexpected character "\\u200B".', + { line: 1, column: 1 }, ); }); @@ -619,9 +671,16 @@ describe('Lexer', () => { end: 1, value: 'a', }); - expect(() => lexer.advance()).to.throw( - 'Syntax Error GraphQL request (1:3) Invalid number, expected digit but got: "b".', + let caughtError; + try { + lexer.advance(); + } catch (error) { + caughtError = error; + } + expect(caughtError.message).to.equal( + 'Syntax Error: Invalid number, expected digit but got: "b".', ); + expect(caughtError.locations).to.deep.equal([{ line: 1, column: 3 }]); }); it('produces double linked list of tokens, including comments', () => { diff --git a/src/language/__tests__/parser-test.js b/src/language/__tests__/parser-test.js index 6c4ef919def..937e901b77f 100644 --- a/src/language/__tests__/parser-test.js +++ b/src/language/__tests__/parser-test.js @@ -14,6 +14,16 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import dedent from '../../jsutils/dedent'; +function expectSyntaxError(text, message, location) { + try { + parse(text); + expect.fail('Expected to throw syntax error'); + } catch (error) { + expect(error.message).to.contain(message); + expect(error.locations).to.deep.equal([location]); + } +} + describe('Parser', () => { it('asserts that a source to parse was provided', () => { expect(() => parse()).to.throw('Must provide Source. Received: undefined'); @@ -33,9 +43,14 @@ describe('Parser', () => { caughtError = error; } - expect(caughtError.message).to.equal(dedent` - Syntax Error GraphQL request (1:2) Expected Name, found + expect(caughtError.message).to.equal( + 'Syntax Error: Expected Name, found ', + ); + + expect(String(caughtError)).to.equal(dedent` + Syntax Error: Expected Name, found + GraphQL request (1:2) 1: { ^ `); @@ -44,32 +59,42 @@ describe('Parser', () => { expect(caughtError.locations).to.deep.equal([{ line: 1, column: 2 }]); - expect(() => - parse(dedent` - { ...MissingOn } - fragment MissingOn Type - `), - ).to.throw( - 'Syntax Error GraphQL request (2:20) Expected "on", found Name "Type"', + expectSyntaxError( + ` + { ...MissingOn } + fragment MissingOn Type`, + 'Expected "on", found Name "Type"', + { line: 3, column: 26 }, ); - expect(() => parse('{ field: {} }')).to.throw( - 'Syntax Error GraphQL request (1:10) Expected Name, found {', - ); + expectSyntaxError('{ field: {} }', 'Expected Name, found {', { + line: 1, + column: 10, + }); - expect(() => parse('notanoperation Foo { field }')).to.throw( - 'Syntax Error GraphQL request (1:1) Unexpected Name "notanoperation"', + expectSyntaxError( + 'notanoperation Foo { field }', + 'Unexpected Name "notanoperation"', + { line: 1, column: 1 }, ); - expect(() => parse('...')).to.throw( - 'Syntax Error GraphQL request (1:1) Unexpected ...', - ); + expectSyntaxError('...', 'Unexpected ...', { line: 1, column: 1 }); }); it('parse provides useful error when using source', () => { - expect(() => parse(new Source('query', 'MyQuery.graphql'))).to.throw( - 'Syntax Error MyQuery.graphql (1:6) Expected {, found ', - ); + let caughtError; + try { + parse(new Source('query', 'MyQuery.graphql')); + } catch (error) { + caughtError = error; + } + expect(String(caughtError)).to.equal(dedent` + Syntax Error: Expected {, found + + MyQuery.graphql (1:6) + 1: query + ^ + `); }); it('parses variable inline values', () => { @@ -79,21 +104,25 @@ describe('Parser', () => { }); it('parses constant default values', () => { - expect(() => - parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }'), - ).to.throw('Syntax Error GraphQL request (1:37) Unexpected $'); + expectSyntaxError( + 'query Foo($x: Complex = { a: { b: [ $var ] } }) { field }', + 'Unexpected $', + { line: 1, column: 37 }, + ); }); it('does not accept fragments named "on"', () => { - expect(() => parse('fragment on on on { on }')).to.throw( - 'Syntax Error GraphQL request (1:10) Unexpected Name "on"', - ); + expectSyntaxError('fragment on on on { on }', 'Unexpected Name "on"', { + line: 1, + column: 10, + }); }); it('does not accept fragments spread of "on"', () => { - expect(() => parse('{ ...on }')).to.throw( - 'Syntax Error GraphQL request (1:9) Expected Name, found }', - ); + expectSyntaxError('{ ...on }', 'Expected Name, found }', { + line: 1, + column: 9, + }); }); it('parses multi-byte characters', async () => { diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 808d30b4598..b94286ed168 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -13,6 +13,16 @@ function printJson(obj) { return JSON.stringify(obj, null, 2); } +function expectSyntaxError(text, message, location) { + try { + parse(text); + expect.fail('Expected to throw syntax error'); + } catch (error) { + expect(error.message).to.contain(message); + expect(error.locations).to.deep.equal([location]); + } +} + function typeNode(name, loc) { return { kind: 'NamedType', @@ -194,30 +204,31 @@ extend type Hello { }); it('Extension without anything throws', () => { - expect(() => - parse(` - extend type Hello - `), - ).to.throw('Syntax Error GraphQL request (3:5) Unexpected '); + expectSyntaxError('extend type Hello', 'Unexpected ', { + line: 1, + column: 18, + }); }); it('Extension do not include descriptions', () => { - expect(() => - parse(` + expectSyntaxError( + ` "Description" extend type Hello { world: String - } - `), - ).to.throw('Syntax Error GraphQL request (3:7)'); + }`, + 'Unexpected Name "extend"', + { line: 3, column: 7 }, + ); - expect(() => - parse(` + expectSyntaxError( + ` extend "Description" type Hello { world: String - } - `), - ).to.throw('Syntax Error GraphQL request (2:14)'); + }`, + 'Unexpected String "Description"', + { line: 2, column: 14 }, + ); }); it('Simple non-null type', () => { @@ -603,23 +614,32 @@ type Hello { }); it('Union fails with no types', () => { - const body = 'union Hello = |'; - expect(() => parse(body)).to.throw(); + expectSyntaxError('union Hello = |', 'Expected Name, found ', { + line: 1, + column: 16, + }); }); it('Union fails with leading douple pipe', () => { - const body = 'union Hello = || Wo | Rld'; - expect(() => parse(body)).to.throw(); + expectSyntaxError('union Hello = || Wo | Rld', 'Expected Name, found |', { + line: 1, + column: 16, + }); }); it('Union fails with double pipe', () => { - const body = 'union Hello = Wo || Rld'; - expect(() => parse(body)).to.throw(); + expectSyntaxError('union Hello = Wo || Rld', 'Expected Name, found |', { + line: 1, + column: 19, + }); }); it('Union fails with trailing pipe', () => { - const body = 'union Hello = | Wo | Rld |'; - expect(() => parse(body)).to.throw(); + expectSyntaxError( + 'union Hello = | Wo | Rld |', + 'Expected Name, found ', + { line: 1, column: 27 }, + ); }); it('Scalar', () => { @@ -670,20 +690,22 @@ input Hello { }); it('Simple input object with args should fail', () => { - const body = ` -input Hello { - world(foo: Int): String -}`; - expect(() => parse(body)).to.throw('Error'); + expectSyntaxError( + ` + input Hello { + world(foo: Int): String + }`, + 'Expected :, found (', + { line: 3, column: 14 }, + ); }); it('Directive with incorrect locations', () => { - expect(() => - parse(` - directive @foo on FIELD | INCORRECT_LOCATION - `), - ).to.throw( - 'Syntax Error GraphQL request (2:33) Unexpected Name "INCORRECT_LOCATION"', + expectSyntaxError( + ` + directive @foo on FIELD | INCORRECT_LOCATION`, + 'Unexpected Name "INCORRECT_LOCATION"', + { line: 2, column: 33 }, ); }); }); diff --git a/src/language/index.js b/src/language/index.js index b2d1c616798..7097734b654 100644 --- a/src/language/index.js +++ b/src/language/index.js @@ -8,6 +8,7 @@ */ export { getLocation } from './location'; +export type { SourceLocation } from './location'; import * as Kind from './kinds'; export { Kind }; export { createLexer, TokenKind } from './lexer'; diff --git a/src/language/location.js b/src/language/location.js index f421bc7afb0..d92a352573f 100644 --- a/src/language/location.js +++ b/src/language/location.js @@ -12,10 +12,10 @@ import type { Source } from './source'; /** * Represents a location in a Source. */ -export type SourceLocation = { - line: number, - column: number, -}; +export type SourceLocation = {| + +line: number, + +column: number, +|}; /** * Takes a Source and a UTF-8 character offset, and returns the corresponding