diff --git a/src/error/GraphQLError.js b/src/error/GraphQLError.js index cb10596433..1d0681e6e6 100644 --- a/src/error/GraphQLError.js +++ b/src/error/GraphQLError.js @@ -1,10 +1,10 @@ // @flow strict import isObjectLike from '../jsutils/isObjectLike'; -import { printError } from './printError'; import { type ASTNode } from '../language/ast'; import { type Source } from '../language/source'; import { type SourceLocation, getLocation } from '../language/location'; +import { printLocation, printSourceLocation } from '../language/printLocation'; /** * A GraphQLError describes an Error found during the parse, validate, or @@ -217,3 +217,25 @@ export function GraphQLError( // eslint-disable-line no-redeclare }, }, }); + +/** + * Prints a GraphQLError to a string, representing useful location information + * about the error's position in the source. + */ +export function printError(error: GraphQLError): string { + let output = error.message; + + if (error.nodes) { + for (const node of error.nodes) { + if (node.loc) { + output += '\n\n' + printLocation(node.loc); + } + } + } else if (error.source && error.locations) { + for (const location of error.locations) { + output += '\n\n' + printSourceLocation(error.source, location); + } + } + + return output; +} diff --git a/src/error/__tests__/GraphQLError-test.js b/src/error/__tests__/GraphQLError-test.js index 1d51c3a196..1a4c720a18 100644 --- a/src/error/__tests__/GraphQLError-test.js +++ b/src/error/__tests__/GraphQLError-test.js @@ -5,7 +5,14 @@ import { describe, it } from 'mocha'; import dedent from '../../jsutils/dedent'; import invariant from '../../jsutils/invariant'; -import { Kind, parse, Source, GraphQLError, formatError } from '../../'; +import { + Kind, + parse, + Source, + GraphQLError, + printError, + formatError, +} from '../../'; const source = new Source(dedent` { @@ -155,3 +162,71 @@ describe('GraphQLError', () => { }); }); }); + +describe('printError', () => { + it('prints an error without location', () => { + const error = new GraphQLError('Error without location'); + expect(printError(error)).to.equal('Error without location'); + }); + + it('prints an error using node without location', () => { + const error = new GraphQLError( + 'Error attached to node without location', + parse('{ foo }', { noLocation: true }), + ); + expect(printError(error)).to.equal( + 'Error attached to node without location', + ); + }); + + it('prints an error with nodes from different sources', () => { + const docA = parse( + new Source( + dedent` + type Foo { + field: String + } + `, + 'SourceA', + ), + ); + const opA = docA.definitions[0]; + invariant(opA && opA.kind === Kind.OBJECT_TYPE_DEFINITION && opA.fields); + const fieldA = opA.fields[0]; + + const docB = parse( + new Source( + dedent` + type Foo { + field: Int + } + `, + 'SourceB', + ), + ); + const opB = docB.definitions[0]; + invariant(opB && opB.kind === Kind.OBJECT_TYPE_DEFINITION && opB.fields); + const fieldB = opB.fields[0]; + + const error = new GraphQLError('Example error with two nodes', [ + fieldA.type, + fieldB.type, + ]); + + expect(printError(error) + '\n').to.equal(dedent` + Example error with two nodes + + SourceA:2:10 + 1: type Foo { + 2: field: String + ^ + 3: } + + SourceB:2:10 + 1: type Foo { + 2: field: Int + ^ + 3: } + `); + }); +}); diff --git a/src/error/__tests__/printError-test.js b/src/error/__tests__/printError-test.js deleted file mode 100644 index e252229d23..0000000000 --- a/src/error/__tests__/printError-test.js +++ /dev/null @@ -1,109 +0,0 @@ -// @flow strict - -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import dedent from '../../jsutils/dedent'; -import invariant from '../../jsutils/invariant'; -import { GraphQLError } from '../GraphQLError'; -import { printError } from '../printError'; -import { Kind, parse, Source } from '../../language'; - -describe('printError', () => { - it('prints an error without location', () => { - const error = new GraphQLError('Error without location'); - expect(printError(error)).to.equal('Error without location'); - }); - - it('prints an error using node without location', () => { - const error = new GraphQLError( - 'Error attached to node without location', - parse('{ foo }', { noLocation: true }), - ); - expect(printError(error)).to.equal( - 'Error attached to node without location', - ); - }); - - it('prints an line numbers with correct padding', () => { - const singleDigit = new GraphQLError( - 'Single digit line number with no padding', - null, - new Source('*', 'Test', { line: 9, column: 1 }), - [0], - ); - expect(printError(singleDigit) + '\n').to.equal(dedent` - Single digit line number with no padding - - Test:9:1 - 9: * - ^ - `); - - const doubleDigit = new GraphQLError( - 'Left padded first line number', - null, - new Source('*\n', 'Test', { line: 9, column: 1 }), - [0], - ); - expect(printError(doubleDigit) + '\n').to.equal(dedent` - Left padded first line number - - Test:9:1 - 9: * - ^ - 10: - `); - }); - - it('prints an error with nodes from different sources', () => { - const docA = parse( - new Source( - dedent` - type Foo { - field: String - } - `, - 'SourceA', - ), - ); - const opA = docA.definitions[0]; - invariant(opA && opA.kind === Kind.OBJECT_TYPE_DEFINITION && opA.fields); - const fieldA = opA.fields[0]; - - const docB = parse( - new Source( - dedent` - type Foo { - field: Int - } - `, - 'SourceB', - ), - ); - const opB = docB.definitions[0]; - invariant(opB && opB.kind === Kind.OBJECT_TYPE_DEFINITION && opB.fields); - const fieldB = opB.fields[0]; - - const error = new GraphQLError('Example error with two nodes', [ - fieldA.type, - fieldB.type, - ]); - - expect(printError(error) + '\n').to.equal(dedent` - Example error with two nodes - - SourceA:2:10 - 1: type Foo { - 2: field: String - ^ - 3: } - - SourceB:2:10 - 1: type Foo { - 2: field: Int - ^ - 3: } - `); - }); -}); diff --git a/src/error/index.js b/src/error/index.js index 1588347424..c920bfc188 100644 --- a/src/error/index.js +++ b/src/error/index.js @@ -1,12 +1,10 @@ // @flow strict -export { GraphQLError } from './GraphQLError'; +export { GraphQLError, printError } from './GraphQLError'; export { syntaxError } from './syntaxError'; export { locatedError } from './locatedError'; -export { printError } from './printError'; - export { formatError } from './formatError'; export type { GraphQLFormattedError } from './formatError'; diff --git a/src/index.js b/src/index.js index 117a7a7c49..b56d72bf1c 100644 --- a/src/index.js +++ b/src/index.js @@ -174,6 +174,9 @@ export type { export { Source, getLocation, + // Print source location + printLocation, + printSourceLocation, // Lex createLexer, TokenKind, diff --git a/src/language/__tests__/printLocation-test.js b/src/language/__tests__/printLocation-test.js new file mode 100644 index 0000000000..80e1b490cf --- /dev/null +++ b/src/language/__tests__/printLocation-test.js @@ -0,0 +1,37 @@ +// @flow strict + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import dedent from '../../jsutils/dedent'; +import { Source } from '../../language'; +import { printSourceLocation } from '../printLocation'; + +describe('printLocation', () => { + it('prints single digit line number with no padding', () => { + const result = printSourceLocation( + new Source('*', 'Test', { line: 9, column: 1 }), + { line: 1, column: 1 }, + ); + + expect(result + '\n').to.equal(dedent` + Test:9:1 + 9: * + ^ + `); + }); + + it('prints an line numbers with correct padding', () => { + const result = printSourceLocation( + new Source('*\n', 'Test', { line: 9, column: 1 }), + { line: 1, column: 1 }, + ); + + expect(result + '\n').to.equal(dedent` + Test:9:1 + 9: * + ^ + 10: + `); + }); +}); diff --git a/src/language/index.js b/src/language/index.js index a27bf36486..3d8a821703 100644 --- a/src/language/index.js +++ b/src/language/index.js @@ -5,6 +5,8 @@ export { Source } from './source'; export { getLocation } from './location'; export type { SourceLocation } from './location'; +export { printLocation, printSourceLocation } from './printLocation'; + export { Kind } from './kinds'; export type { KindEnum } from './kinds'; diff --git a/src/error/printError.js b/src/language/printLocation.js similarity index 51% rename from src/error/printError.js rename to src/language/printLocation.js index 0c9ddbab75..69b1928e2a 100644 --- a/src/error/printError.js +++ b/src/language/printLocation.js @@ -1,54 +1,35 @@ // @flow strict -import { type SourceLocation, getLocation } from '../language/location'; import { type Source } from '../language/source'; -import { type GraphQLError } from './GraphQLError'; +import { type Location } from '../language/ast'; +import { type SourceLocation, getLocation } from '../language/location'; /** - * Prints a GraphQLError to a string, representing useful location information - * about the error's position in the source. + * Render a helpful description of the location in the GraphQL Source document. */ -export function printError(error: GraphQLError): string { - const printedLocations = []; - if (error.nodes) { - for (const node of error.nodes) { - if (node.loc) { - printedLocations.push( - highlightSourceAtLocation( - node.loc.source, - getLocation(node.loc.source, node.loc.start), - ), - ); - } - } - } else if (error.source && error.locations) { - const source = error.source; - for (const location of error.locations) { - printedLocations.push(highlightSourceAtLocation(source, location)); - } - } - return printedLocations.length === 0 - ? error.message - : [error.message, ...printedLocations].join('\n\n'); +export function printLocation(location: Location): string { + return printSourceLocation( + location.source, + getLocation(location.source, location.start), + ); } /** - * Render a helpful description of the location of the error in the GraphQL - * Source document. + * Render a helpful description of the location in the GraphQL Source document. */ -function highlightSourceAtLocation( +export function printSourceLocation( source: Source, - location: SourceLocation, + sourceLocation: SourceLocation, ): string { const firstLineColumnOffset = source.locationOffset.column - 1; const body = whitespace(firstLineColumnOffset) + source.body; - const lineIndex = location.line - 1; + const lineIndex = sourceLocation.line - 1; const lineOffset = source.locationOffset.line - 1; - const lineNum = location.line + lineOffset; + const lineNum = sourceLocation.line + lineOffset; - const columnOffset = location.line === 1 ? firstLineColumnOffset : 0; - const columnNum = location.column + columnOffset; + const columnOffset = sourceLocation.line === 1 ? firstLineColumnOffset : 0; + const columnNum = sourceLocation.column + columnOffset; const lines = body.split(/\r\n|[\n\r]/g); return (