Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract 'printLocation' & 'printSourceLocation' functions #1984

Merged
merged 1 commit into from Jun 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 23 additions & 1 deletion 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
Expand Down Expand Up @@ -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;
}
77 changes: 76 additions & 1 deletion src/error/__tests__/GraphQLError-test.js
Expand Up @@ -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`
{
Expand Down Expand Up @@ -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: }
`);
});
});
109 changes: 0 additions & 109 deletions src/error/__tests__/printError-test.js

This file was deleted.

4 changes: 1 addition & 3 deletions 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';
3 changes: 3 additions & 0 deletions src/index.js
Expand Up @@ -174,6 +174,9 @@ export type {
export {
Source,
getLocation,
// Print source location
printLocation,
printSourceLocation,
// Lex
createLexer,
TokenKind,
Expand Down
37 changes: 37 additions & 0 deletions 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:
`);
});
});
2 changes: 2 additions & 0 deletions src/language/index.js
Expand Up @@ -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';

Expand Down
49 changes: 15 additions & 34 deletions src/error/printError.js → 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 (
Expand Down