import { expect } from 'chai';
import { describe, it } from 'mocha';
import { dedent } from '../../__testUtils__/dedent';
import { expectJSON, expectToThrowJSON } from '../../__testUtils__/expectJSON';
import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery';
import { inspect } from '../../jsutils/inspect';
import { Kind } from '../kinds';
import {
parse,
parseConstValue,
parseSchemaCoordinate,
parseType,
parseValue,
} from '../parser';
import { Source } from '../source';
import { TokenKind } from '../tokenKind';
function expectSyntaxError(text: string) {
return expectToThrowJSON(() => parse(text));
}
describe('Parser', () => {
it('parse provides useful errors', () => {
let caughtError;
try {
parse('{');
} catch (error) {
caughtError = error;
}
expect(caughtError).to.deep.contain({
message: 'Syntax Error: Expected Name, found <EOF>.',
positions: [1],
locations: [{ line: 1, column: 2 }],
});
expect(String(caughtError)).to.equal(dedent`
Syntax Error: Expected Name, found <EOF>.
GraphQL request:1:2
1 | {
| ^
`);
expectSyntaxError(`
{ ...MissingOn }
fragment MissingOn Type
`).to.deep.include({
message: 'Syntax Error: Expected "on", found Name "Type".',
locations: [{ line: 3, column: 26 }],
});
expectSyntaxError('{ field: {} }').to.deep.include({
message: 'Syntax Error: Expected Name, found "{".',
locations: [{ line: 1, column: 10 }],
});
expectSyntaxError('notAnOperation Foo { field }').to.deep.include({
message: 'Syntax Error: Unexpected Name "notAnOperation".',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('...').to.deep.include({
message: 'Syntax Error: Unexpected "...".',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('{ ""').to.deep.include({
message: 'Syntax Error: Expected Name, found String "".',
locations: [{ line: 1, column: 3 }],
});
});
it('parse provides useful error when using source', () => {
let caughtError;
try {
parse(new Source('query', 'MyQuery.graphql'));
} catch (error) {
caughtError = error;
}
expect(String(caughtError)).to.equal(dedent`
Syntax Error: Expected "{", found <EOF>.
MyQuery.graphql:1:6
1 | query
| ^
`);
});
it('exposes the tokenCount', () => {
expect(parse('{ foo }').tokenCount).to.equal(3);
expect(parse('{ foo(bar: "baz") }').tokenCount).to.equal(8);
});
it('limit maximum number of tokens', () => {
expect(() => parse('{ foo }', { maxTokens: 3 })).to.not.throw();
expect(() => parse('{ foo }', { maxTokens: 2 })).to.throw(
'Syntax Error: Document contains more that 2 tokens. Parsing aborted.',
);
expect(() => parse('{ foo(bar: "baz") }', { maxTokens: 8 })).to.not.throw();
expect(() => parse('{ foo(bar: "baz") }', { maxTokens: 7 })).to.throw(
'Syntax Error: Document contains more that 7 tokens. Parsing aborted.',
);
});
it('parses variable inline values', () => {
expect(() =>
parse('{ field(complex: { a: { b: [ $var ] } }) }'),
).to.not.throw();
});
it('parses constant default values', () => {
expectSyntaxError(
'query Foo($x: Complex = { a: { b: [ $var ] } }) { field }',
).to.deep.equal({
message: 'Syntax Error: Unexpected variable "$var" in constant value.',
locations: [{ line: 1, column: 37 }],
});
});
it('parses variable definition directives', () => {
expect(() =>
parse('query Foo($x: Boolean = false @bar) { field }'),
).to.not.throw();
});
it('does not accept fragments named "on"', () => {
expectSyntaxError('fragment on on on { on }').to.deep.equal({
message: 'Syntax Error: Unexpected Name "on".',
locations: [{ line: 1, column: 10 }],
});
});
it('does not accept fragments spread of "on"', () => {
expectSyntaxError('{ ...on }').to.deep.equal({
message: 'Syntax Error: Expected Name, found "}".',
locations: [{ line: 1, column: 9 }],
});
});
it('does not allow "true", "false", or "null" as enum value', () => {
expectSyntaxError('enum Test { VALID, true }').to.deep.equal({
message:
'Syntax Error: Name "true" is reserved and cannot be used for an enum value.',
locations: [{ line: 1, column: 20 }],
});
expectSyntaxError('enum Test { VALID, false }').to.deep.equal({
message:
'Syntax Error: Name "false" is reserved and cannot be used for an enum value.',
locations: [{ line: 1, column: 20 }],
});
expectSyntaxError('enum Test { VALID, null }').to.deep.equal({
message:
'Syntax Error: Name "null" is reserved and cannot be used for an enum value.',
locations: [{ line: 1, column: 20 }],
});
});
it('parses multi-byte characters', () => {
const ast = parse(`
# This comment has a \u0A0A multi-byte character.
{ field(arg: "Has a \u0A0A multi-byte character.") }
`);
expect(ast).to.have.nested.property(
'definitions[0].selectionSet.selections[0].arguments[0].value.value',
'Has a \u0A0A multi-byte character.',
);
});
it('parses kitchen sink', () => {
expect(() => parse(kitchenSinkQuery)).to.not.throw();
});
it('allows non-keywords anywhere a Name is allowed', () => {
const nonKeywords = [
'on',
'fragment',
'query',
'mutation',
'subscription',
'true',
'false',
];
for (const keyword of nonKeywords) {
const fragmentName = keyword !== 'on' ? keyword : 'a';
const document = `
query ${keyword} {
... ${fragmentName}
... on ${keyword} { field }
}
fragment ${fragmentName} on Type {
${keyword}(${keyword}: $${keyword})
@${keyword}(${keyword}: ${keyword})
}
`;
expect(() => parse(document)).to.not.throw();
}
});
it('parses anonymous mutation operations', () => {
expect(() =>
parse(`
mutation {
mutationField
}
`),
).to.not.throw();
});
it('parses anonymous subscription operations', () => {
expect(() =>
parse(`
subscription {
subscriptionField
}
`),
).to.not.throw();
});
it('parses named mutation operations', () => {
expect(() =>
parse(`
mutation Foo {
mutationField
}
`),
).to.not.throw();
});
it('parses named subscription operations', () => {
expect(() =>
parse(`
subscription Foo {
subscriptionField
}
`),
).to.not.throw();
});
it('creates ast', () => {
const result = parse(dedent`
{
node(id: 4) {
id,
name
}
}
`);
expectJSON(result).toDeepEqual({
kind: Kind.DOCUMENT,
loc: { start: 0, end: 40 },
definitions: [
{
kind: Kind.OPERATION_DEFINITION,
description: undefined,
loc: { start: 0, end: 40 },
operation: 'query',
name: undefined,
variableDefinitions: [],
directives: [],
selectionSet: {
kind: Kind.SELECTION_SET,
loc: { start: 0, end: 40 },
selections: [
{
kind: Kind.FIELD,
loc: { start: 4, end: 38 },
alias: undefined,
name: {
kind: Kind.NAME,
loc: { start: 4, end: 8 },
value: 'node',
},
arguments: [
{
kind: Kind.ARGUMENT,
name: {
kind: Kind.NAME,
loc: { start: 9, end: 11 },
value: 'id',
},
value: {
kind: Kind.INT,
loc: { start: 13, end: 14 },
value: '4',
},
loc: { start: 9, end: 14 },
},
],
directives: [],
selectionSet: {
kind: Kind.SELECTION_SET,
loc: { start: 16, end: 38 },
selections: [
{
kind: Kind.FIELD,
loc: { start: 22, end: 24 },
alias: undefined,
name: {
kind: Kind.NAME,
loc: { start: 22, end: 24 },
value: 'id',
},
arguments: [],
directives: [],
selectionSet: undefined,
},
{
kind: Kind.FIELD,
loc: { start: 30, end: 34 },
alias: undefined,
name: {
kind: Kind.NAME,
loc: { start: 30, end: 34 },
value: 'name',
},
arguments: [],
directives: [],
selectionSet: undefined,
},
],
},
},
],
},
},
],
});
});
it('creates ast from nameless query without variables', () => {
const result = parse(dedent`
query {
node {
id
}
}
`);
expectJSON(result).toDeepEqual({
kind: Kind.DOCUMENT,
loc: { start: 0, end: 29 },
definitions: [
{
kind: Kind.OPERATION_DEFINITION,
loc: { start: 0, end: 29 },
description: undefined,
operation: 'query',
name: undefined,
variableDefinitions: [],
directives: [],
selectionSet: {
kind: Kind.SELECTION_SET,
loc: { start: 6, end: 29 },
selections: [
{
kind: Kind.FIELD,
loc: { start: 10, end: 27 },
alias: undefined,
name: {
kind: Kind.NAME,
loc: { start: 10, end: 14 },
value: 'node',
},
arguments: [],
directives: [],
selectionSet: {
kind: Kind.SELECTION_SET,
loc: { start: 15, end: 27 },
selections: [
{
kind: Kind.FIELD,
loc: { start: 21, end: 23 },
alias: undefined,
name: {
kind: Kind.NAME,
loc: { start: 21, end: 23 },
value: 'id',
},
arguments: [],
directives: [],
selectionSet: undefined,
},
],
},
},
],
},
},
],
});
});
it('creates ast from nameless query with description', () => {
const result = parse(dedent`
"Description"
query {
node {
id
}
}
`);
expectJSON(result).toDeepEqual({
kind: Kind.DOCUMENT,
loc: { start: 0, end: 43 },
definitions: [
{
kind: Kind.OPERATION_DEFINITION,
loc: { start: 0, end: 43 },
description: {
kind: Kind.STRING,
loc: { start: 0, end: 13 },
value: 'Description',
block: false,
},
operation: 'query',
name: undefined,
variableDefinitions: [],
directives: [],
selectionSet: {
kind: Kind.SELECTION_SET,
loc: { start: 20, end: 43 },
selections: [
{
kind: Kind.FIELD,
loc: { start: 24, end: 41 },
alias: undefined,
name: {
kind: Kind.NAME,
loc: { start: 24, end: 28 },
value: 'node',
},
arguments: [],
directives: [],
selectionSet: {
kind: Kind.SELECTION_SET,
loc: { start: 29, end: 41 },
selections: [
{
kind: Kind.FIELD,
loc: { start: 35, end: 37 },
alias: undefined,
name: {
kind: Kind.NAME,
loc: { start: 35, end: 37 },
value: 'id',
},
arguments: [],
directives: [],
selectionSet: undefined,
},
],
},
},
],
},
},
],
});
});
it('allows parsing without source location information', () => {
const result = parse('{ id }', { noLocation: true });
expect('loc' in result).to.equal(false);
});
it('Legacy: allows parsing fragment defined variables', () => {
const document = 'fragment a($v: Boolean = false) on t { f(v: $v) }';
expect(() =>
parse(document, { allowLegacyFragmentVariables: true }),
).to.not.throw();
expect(() => parse(document)).to.throw('Syntax Error');
});
it('contains location that can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => {
const { loc } = parse('{ id }');
expect(Object.prototype.toString.call(loc)).to.equal('[object Location]');
expect(JSON.stringify(loc)).to.equal('{"start":0,"end":6}');
expect(inspect(loc)).to.equal('{ start: 0, end: 6 }');
});
it('contains references to source', () => {
const source = new Source('{ id }');
const result = parse(source);
expect(result).to.have.nested.property('loc.source', source);
});
it('contains references to start and end tokens', () => {
const result = parse('{ id }');
expect(result).to.have.nested.property(
'loc.startToken.kind',
TokenKind.SOF,
);
expect(result).to.have.nested.property('loc.endToken.kind', TokenKind.EOF);
});
describe('parseValue', () => {
it('parses null value', () => {
const result = parseValue('null');
expectJSON(result).toDeepEqual({
kind: Kind.NULL,
loc: { start: 0, end: 4 },
});
});
it('parses list values', () => {
const result = parseValue('[123 "abc"]');
expectJSON(result).toDeepEqual({
kind: Kind.LIST,
loc: { start: 0, end: 11 },
values: [
{
kind: Kind.INT,
loc: { start: 1, end: 4 },
value: '123',
},
{
kind: Kind.STRING,
loc: { start: 5, end: 10 },
value: 'abc',
block: false,
},
],
});
});
it('parses block strings', () => {
const result = parseValue('["""long""" "short"]');
expectJSON(result).toDeepEqual({
kind: Kind.LIST,
loc: { start: 0, end: 20 },
values: [
{
kind: Kind.STRING,
loc: { start: 1, end: 11 },
value: 'long',
block: true,
},
{
kind: Kind.STRING,
loc: { start: 12, end: 19 },
value: 'short',
block: false,
},
],
});
});
it('allows variables', () => {
const result = parseValue('{ field: $var }');
expectJSON(result).toDeepEqual({
kind: Kind.OBJECT,
loc: { start: 0, end: 15 },
fields: [
{
kind: Kind.OBJECT_FIELD,
loc: { start: 2, end: 13 },
name: {
kind: Kind.NAME,
loc: { start: 2, end: 7 },
value: 'field',
},
value: {
kind: Kind.VARIABLE,
loc: { start: 9, end: 13 },
name: {
kind: Kind.NAME,
loc: { start: 10, end: 13 },
value: 'var',
},
},
},
],
});
});
it('correct message for incomplete variable', () => {
expect(() => parseValue('$'))
.to.throw()
.to.deep.include({
message: 'Syntax Error: Expected Name, found <EOF>.',
locations: [{ line: 1, column: 2 }],
});
});
it('correct message for unexpected token', () => {
expect(() => parseValue(':'))
.to.throw()
.to.deep.include({
message: 'Syntax Error: Unexpected ":".',
locations: [{ line: 1, column: 1 }],
});
});
});
describe('parseConstValue', () => {
it('parses values', () => {
const result = parseConstValue('[123 "abc"]');
expectJSON(result).toDeepEqual({
kind: Kind.LIST,
loc: { start: 0, end: 11 },
values: [
{
kind: Kind.INT,
loc: { start: 1, end: 4 },
value: '123',
},
{
kind: Kind.STRING,
loc: { start: 5, end: 10 },
value: 'abc',
block: false,
},
],
});
});
it('does not allow variables', () => {
expect(() => parseConstValue('{ field: $var }'))
.to.throw()
.to.deep.include({
message:
'Syntax Error: Unexpected variable "$var" in constant value.',
locations: [{ line: 1, column: 10 }],
});
});
it('correct message for unexpected token', () => {
expect(() => parseConstValue('$'))
.to.throw()
.to.deep.include({
message: 'Syntax Error: Unexpected "$".',
locations: [{ line: 1, column: 1 }],
});
});
});
describe('parseType', () => {
it('parses well known types', () => {
const result = parseType('String');
expectJSON(result).toDeepEqual({
kind: Kind.NAMED_TYPE,
loc: { start: 0, end: 6 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'String',
},
});
});
it('parses custom types', () => {
const result = parseType('MyType');
expectJSON(result).toDeepEqual({
kind: Kind.NAMED_TYPE,
loc: { start: 0, end: 6 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
});
});
it('parses list types', () => {
const result = parseType('[MyType]');
expectJSON(result).toDeepEqual({
kind: Kind.LIST_TYPE,
loc: { start: 0, end: 8 },
type: {
kind: Kind.NAMED_TYPE,
loc: { start: 1, end: 7 },
name: {
kind: Kind.NAME,
loc: { start: 1, end: 7 },
value: 'MyType',
},
},
});
});
it('parses non-null types', () => {
const result = parseType('MyType!');
expectJSON(result).toDeepEqual({
kind: Kind.NON_NULL_TYPE,
loc: { start: 0, end: 7 },
type: {
kind: Kind.NAMED_TYPE,
loc: { start: 0, end: 6 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
},
});
});
it('parses nested types', () => {
const result = parseType('[MyType!]');
expectJSON(result).toDeepEqual({
kind: Kind.LIST_TYPE,
loc: { start: 0, end: 9 },
type: {
kind: Kind.NON_NULL_TYPE,
loc: { start: 1, end: 8 },
type: {
kind: Kind.NAMED_TYPE,
loc: { start: 1, end: 7 },
name: {
kind: Kind.NAME,
loc: { start: 1, end: 7 },
value: 'MyType',
},
},
},
});
});
});
describe('parseSchemaCoordinate', () => {
it('parses Name', () => {
const result = parseSchemaCoordinate('MyType');
expectJSON(result).toDeepEqual({
kind: Kind.TYPE_COORDINATE,
loc: { start: 0, end: 6 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
});
});
it('parses Name . Name', () => {
const result = parseSchemaCoordinate('MyType.field');
expectJSON(result).toDeepEqual({
kind: Kind.MEMBER_COORDINATE,
loc: { start: 0, end: 12 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
memberName: {
kind: Kind.NAME,
loc: { start: 7, end: 12 },
value: 'field',
},
});
});
it('rejects Name . Name . Name', () => {
expect(() => parseSchemaCoordinate('MyType.field.deep'))
.to.throw()
.to.deep.include({
message: 'Syntax Error: Expected <EOF>, found ".".',
locations: [{ line: 1, column: 13 }],
});
});
it('parses Name . Name ( Name : )', () => {
const result = parseSchemaCoordinate('MyType.field(arg:)');
expectJSON(result).toDeepEqual({
kind: Kind.ARGUMENT_COORDINATE,
loc: { start: 0, end: 18 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
fieldName: {
kind: Kind.NAME,
loc: { start: 7, end: 12 },
value: 'field',
},
argumentName: {
kind: Kind.NAME,
loc: { start: 13, end: 16 },
value: 'arg',
},
});
});
it('rejects Name . Name ( Name : Name )', () => {
expect(() => parseSchemaCoordinate('MyType.field(arg: value)'))
.to.throw()
.to.deep.include({
message: 'Syntax Error: Invalid character: " ".',
locations: [{ line: 1, column: 18 }],
});
});
it('parses @ Name', () => {
const result = parseSchemaCoordinate('@myDirective');
expectJSON(result).toDeepEqual({
kind: Kind.DIRECTIVE_COORDINATE,
loc: { start: 0, end: 12 },
name: {
kind: Kind.NAME,
loc: { start: 1, end: 12 },
value: 'myDirective',
},
});
});
it('parses @ Name ( Name : )', () => {
const result = parseSchemaCoordinate('@myDirective(arg:)');
expectJSON(result).toDeepEqual({
kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE,
loc: { start: 0, end: 18 },
name: {
kind: Kind.NAME,
loc: { start: 1, end: 12 },
value: 'myDirective',
},
argumentName: {
kind: Kind.NAME,
loc: { start: 13, end: 16 },
value: 'arg',
},
});
});
it('parses __Type', () => {
const result = parseSchemaCoordinate('__Type');
expectJSON(result).toDeepEqual({
kind: Kind.TYPE_COORDINATE,
loc: { start: 0, end: 6 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: '__Type',
},
});
});
it('parses Type.__metafield', () => {
const result = parseSchemaCoordinate('Type.__metafield');
expectJSON(result).toDeepEqual({
kind: Kind.MEMBER_COORDINATE,
loc: { start: 0, end: 16 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 4 },
value: 'Type',
},
memberName: {
kind: Kind.NAME,
loc: { start: 5, end: 16 },
value: '__metafield',
},
});
});
it('parses Type.__metafield(arg:)', () => {
const result = parseSchemaCoordinate('Type.__metafield(arg:)');
expectJSON(result).toDeepEqual({
kind: Kind.ARGUMENT_COORDINATE,
loc: { start: 0, end: 22 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 4 },
value: 'Type',
},
fieldName: {
kind: Kind.NAME,
loc: { start: 5, end: 16 },
value: '__metafield',
},
argumentName: {
kind: Kind.NAME,
loc: { start: 17, end: 20 },
value: 'arg',
},
});
});
it('rejects @ Name . Name', () => {
expect(() => parseSchemaCoordinate('@myDirective.field'))
.to.throw()
.to.deep.include({
message: 'Syntax Error: Expected <EOF>, found ".".',
locations: [{ line: 1, column: 13 }],
});
});
it('accepts a Source object', () => {
expect(parseSchemaCoordinate('MyType')).to.deep.equal(
parseSchemaCoordinate(new Source('MyType')),
);
});
});
describe('operation and variable definition descriptions', () => {
it('parses operation with description and variable descriptions', () => {
const result = parse(dedent`
"Operation description"
query myQuery(
"Variable a description"
$a: Int,
"""Variable b\nmultiline description"""
$b: String
) {
field(a: $a, b: $b)
}
`);
const opDef = result.definitions.find(
(d) => d.kind === Kind.OPERATION_DEFINITION,
);
if (!opDef || opDef.kind !== Kind.OPERATION_DEFINITION) {
throw new Error('No operation definition found');
}
expectJSON(opDef).toDeepEqual({
kind: Kind.OPERATION_DEFINITION,
operation: 'query',
description: {
kind: Kind.STRING,
value: 'Operation description',
block: false,
loc: { start: 0, end: 23 },
},
name: {
kind: Kind.NAME,
value: 'myQuery',
loc: { start: 30, end: 37 },
},
variableDefinitions: [
{
kind: Kind.VARIABLE_DEFINITION,
description: {
kind: Kind.STRING,
value: 'Variable a description',
block: false,
loc: { start: 41, end: 65 },
},
variable: {
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
value: 'a',
loc: { start: 69, end: 70 },
},
loc: { start: 68, end: 70 },
},
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: 'Int',
loc: { start: 72, end: 75 },
},
loc: { start: 72, end: 75 },
},
defaultValue: undefined,
directives: [],
loc: { start: 41, end: 75 },
},
{
kind: Kind.VARIABLE_DEFINITION,
description: {
kind: Kind.STRING,
value: 'Variable b\nmultiline description',
block: true,
loc: { start: 79, end: 117 },
},
variable: {
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
value: 'b',
loc: { start: 121, end: 122 },
},
loc: { start: 120, end: 122 },
},
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: 'String',
loc: { start: 124, end: 130 },
},
loc: { start: 124, end: 130 },
},
defaultValue: undefined,
directives: [],
loc: { start: 79, end: 130 },
},
],
directives: [],
selectionSet: {
kind: Kind.SELECTION_SET,
selections: [
{
kind: Kind.FIELD,
alias: undefined,
name: {
kind: Kind.NAME,
value: 'field',
loc: { start: 137, end: 142 },
},
arguments: [
{
kind: Kind.ARGUMENT,
name: {
kind: Kind.NAME,
value: 'a',
loc: { start: 143, end: 144 },
},
value: {
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
value: 'a',
loc: { start: 147, end: 148 },
},
loc: { start: 146, end: 148 },
},
loc: { start: 143, end: 148 },
},
{
kind: Kind.ARGUMENT,
name: {
kind: Kind.NAME,
value: 'b',
loc: { start: 150, end: 151 },
},
value: {
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
value: 'b',
loc: { start: 154, end: 155 },
},
loc: { start: 153, end: 155 },
},
loc: { start: 150, end: 155 },
},
],
directives: [],
selectionSet: undefined,
loc: { start: 137, end: 156 },
},
],
loc: { start: 133, end: 158 },
},
loc: { start: 0, end: 158 },
});
});
it('descriptions on a short-hand query produce a sensible error', () => {
const input = `"""Invalid"""
{ __typename }`;
expect(() => parse(input)).to.throw(
'Syntax Error: Unexpected description, descriptions are not supported on shorthand queries.',
);
});
it('parses variable definition with description, default value, and directives', () => {
const result = parse(dedent`
query (
"desc"
$foo: Int = 42 @dir
) {
field(foo: $foo)
}
`);
const opDef = result.definitions.find(
(d) => d.kind === Kind.OPERATION_DEFINITION,
);
if (!opDef || opDef.kind !== Kind.OPERATION_DEFINITION) {
throw new Error('No operation definition found');
}
const varDef = opDef.variableDefinitions?.[0];
expectJSON(varDef).toDeepEqual({
kind: Kind.VARIABLE_DEFINITION,
defaultValue: {
kind: Kind.INT,
value: '42',
loc: { start: 31, end: 33 },
},
directives: [
{
arguments: [],
kind: Kind.DIRECTIVE,
name: {
kind: Kind.NAME,
value: 'dir',
loc: { start: 35, end: 38 },
},
loc: { start: 34, end: 38 },
},
],
description: {
kind: Kind.STRING,
value: 'desc',
block: false,
loc: { start: 10, end: 16 },
},
variable: {
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
value: 'foo',
loc: { start: 20, end: 23 },
},
loc: { start: 19, end: 23 },
},
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: 'Int',
loc: { start: 25, end: 28 },
},
loc: { start: 25, end: 28 },
},
loc: { start: 10, end: 38 },
});
});
it('parses fragment with variable description (legacy)', () => {
const result = parse('fragment Foo("desc" $foo: Int) on Bar { baz }', {
allowLegacyFragmentVariables: true,
});
const fragDef = result.definitions.find(
(d) => d.kind === Kind.FRAGMENT_DEFINITION,
);
if (!fragDef || fragDef.kind !== Kind.FRAGMENT_DEFINITION) {
throw new Error('No fragment definition found');
}
const varDef = fragDef.variableDefinitions?.[0];
expectJSON(varDef).toDeepEqual({
kind: Kind.VARIABLE_DEFINITION,
description: {
kind: Kind.STRING,
value: 'desc',
block: false,
loc: { start: 13, end: 19 },
},
variable: {
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
value: 'foo',
loc: { start: 21, end: 24 },
},
loc: { start: 20, end: 24 },
},
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: 'Int',
loc: { start: 26, end: 29 },
},
loc: { start: 26, end: 29 },
},
defaultValue: undefined,
directives: [],
loc: { start: 13, end: 29 },
});
});
});
});