blob: 2fdcdc6ab64366298aa0a92c141503e0b47e7163 [file] [log] [blame]
// @flow strict
import inspect from '../jsutils/inspect';
import devAssert from '../jsutils/devAssert';
import { syntaxError } from '../error/syntaxError';
import { type GraphQLError } from '../error/GraphQLError';
import { Kind } from './kinds';
import { Source } from './source';
import { DirectiveLocation } from './directiveLocation';
import { type TokenKindEnum, TokenKind } from './tokenKind';
import { Lexer, isPunctuatorTokenKind } from './lexer';
import {
Location,
type Token,
type NameNode,
type VariableNode,
type DocumentNode,
type DefinitionNode,
type OperationDefinitionNode,
type OperationTypeNode,
type VariableDefinitionNode,
type SelectionSetNode,
type SelectionNode,
type FieldNode,
type ArgumentNode,
type FragmentSpreadNode,
type InlineFragmentNode,
type FragmentDefinitionNode,
type ValueNode,
type StringValueNode,
type ListValueNode,
type ObjectValueNode,
type ObjectFieldNode,
type DirectiveNode,
type TypeNode,
type NamedTypeNode,
type TypeSystemDefinitionNode,
type SchemaDefinitionNode,
type OperationTypeDefinitionNode,
type ScalarTypeDefinitionNode,
type ObjectTypeDefinitionNode,
type FieldDefinitionNode,
type InputValueDefinitionNode,
type InterfaceTypeDefinitionNode,
type UnionTypeDefinitionNode,
type EnumTypeDefinitionNode,
type EnumValueDefinitionNode,
type InputObjectTypeDefinitionNode,
type DirectiveDefinitionNode,
type TypeSystemExtensionNode,
type SchemaExtensionNode,
type ScalarTypeExtensionNode,
type ObjectTypeExtensionNode,
type InterfaceTypeExtensionNode,
type UnionTypeExtensionNode,
type EnumTypeExtensionNode,
type InputObjectTypeExtensionNode,
} from './ast';
/**
* Configuration options to control parser behavior
*/
export type ParseOptions = {|
/**
* By default, the parser creates AST nodes that know the location
* in the source that they correspond to. This configuration flag
* disables that behavior for performance or testing.
*/
noLocation?: boolean,
/**
* If enabled, the parser will parse empty fields sets in the Schema
* Definition Language. Otherwise, the parser will follow the current
* specification.
*
* This option is provided to ease adoption of the final SDL specification
* and will be removed in v16.
*/
allowLegacySDLEmptyFields?: boolean,
/**
* If enabled, the parser will parse implemented interfaces with no `&`
* character between each interface. Otherwise, the parser will follow the
* current specification.
*
* This option is provided to ease adoption of the final SDL specification
* and will be removed in v16.
*/
allowLegacySDLImplementsInterfaces?: boolean,
/**
* EXPERIMENTAL:
*
* If enabled, the parser will understand and parse variable definitions
* contained in a fragment definition. They'll be represented in the
* `variableDefinitions` field of the FragmentDefinitionNode.
*
* The syntax is identical to normal, query-defined variables. For example:
*
* fragment A($var: Boolean = false) on T {
* ...
* }
*
* Note: this feature is experimental and may change or be removed in the
* future.
*/
experimentalFragmentVariables?: boolean,
|};
/**
* Given a GraphQL source, parses it into a Document.
* Throws GraphQLError if a syntax error is encountered.
*/
export function parse(
source: string | Source,
options?: ParseOptions,
): DocumentNode {
const parser = new Parser(source, options);
return parser.parseDocument();
}
/**
* Given a string containing a GraphQL value (ex. `[42]`), parse the AST for
* that value.
* Throws GraphQLError if a syntax error is encountered.
*
* This is useful within tools that operate upon GraphQL Values directly and
* in isolation of complete GraphQL documents.
*
* Consider providing the results to the utility function: valueFromAST().
*/
export function parseValue(
source: string | Source,
options?: ParseOptions,
): ValueNode {
const parser = new Parser(source, options);
parser.expectToken(TokenKind.SOF);
const value = parser.parseValueLiteral(false);
parser.expectToken(TokenKind.EOF);
return value;
}
/**
* Given a string containing a GraphQL Type (ex. `[Int!]`), parse the AST for
* that type.
* Throws GraphQLError if a syntax error is encountered.
*
* This is useful within tools that operate upon GraphQL Types directly and
* in isolation of complete GraphQL documents.
*
* Consider providing the results to the utility function: typeFromAST().
*/
export function parseType(
source: string | Source,
options?: ParseOptions,
): TypeNode {
const parser = new Parser(source, options);
parser.expectToken(TokenKind.SOF);
const type = parser.parseTypeReference();
parser.expectToken(TokenKind.EOF);
return type;
}
class Parser {
_options: ?ParseOptions;
_lexer: Lexer;
constructor(source: string | Source, options?: ParseOptions) {
const sourceObj = typeof source === 'string' ? new Source(source) : source;
devAssert(
sourceObj instanceof Source,
`Must provide Source. Received: ${inspect(sourceObj)}.`,
);
this._lexer = new Lexer(sourceObj);
this._options = options;
}
/**
* Converts a name lex token into a name parse node.
*/
parseName(): NameNode {
const token = this.expectToken(TokenKind.NAME);
return {
kind: Kind.NAME,
value: ((token.value: any): string),
loc: this.loc(token),
};
}
// Implements the parsing rules in the Document section.
/**
* Document : Definition+
*/
parseDocument(): DocumentNode {
const start = this._lexer.token;
return {
kind: Kind.DOCUMENT,
definitions: this.many(
TokenKind.SOF,
this.parseDefinition,
TokenKind.EOF,
),
loc: this.loc(start),
};
}
/**
* Definition :
* - ExecutableDefinition
* - TypeSystemDefinition
* - TypeSystemExtension
*
* ExecutableDefinition :
* - OperationDefinition
* - FragmentDefinition
*/
parseDefinition(): DefinitionNode {
if (this.peek(TokenKind.NAME)) {
switch (this._lexer.token.value) {
case 'query':
case 'mutation':
case 'subscription':
return this.parseOperationDefinition();
case 'fragment':
return this.parseFragmentDefinition();
case 'schema':
case 'scalar':
case 'type':
case 'interface':
case 'union':
case 'enum':
case 'input':
case 'directive':
return this.parseTypeSystemDefinition();
case 'extend':
return this.parseTypeSystemExtension();
}
} else if (this.peek(TokenKind.BRACE_L)) {
return this.parseOperationDefinition();
} else if (this.peekDescription()) {
return this.parseTypeSystemDefinition();
}
throw this.unexpected();
}
// Implements the parsing rules in the Operations section.
/**
* OperationDefinition :
* - SelectionSet
* - OperationType Name? VariableDefinitions? Directives? SelectionSet
*/
parseOperationDefinition(): OperationDefinitionNode {
const start = this._lexer.token;
if (this.peek(TokenKind.BRACE_L)) {
return {
kind: Kind.OPERATION_DEFINITION,
operation: 'query',
name: undefined,
variableDefinitions: [],
directives: [],
selectionSet: this.parseSelectionSet(),
loc: this.loc(start),
};
}
const operation = this.parseOperationType();
let name;
if (this.peek(TokenKind.NAME)) {
name = this.parseName();
}
return {
kind: Kind.OPERATION_DEFINITION,
operation,
name,
variableDefinitions: this.parseVariableDefinitions(),
directives: this.parseDirectives(false),
selectionSet: this.parseSelectionSet(),
loc: this.loc(start),
};
}
/**
* OperationType : one of query mutation subscription
*/
parseOperationType(): OperationTypeNode {
const operationToken = this.expectToken(TokenKind.NAME);
switch (operationToken.value) {
case 'query':
return 'query';
case 'mutation':
return 'mutation';
case 'subscription':
return 'subscription';
}
throw this.unexpected(operationToken);
}
/**
* VariableDefinitions : ( VariableDefinition+ )
*/
parseVariableDefinitions(): Array<VariableDefinitionNode> {
return this.optionalMany(
TokenKind.PAREN_L,
this.parseVariableDefinition,
TokenKind.PAREN_R,
);
}
/**
* VariableDefinition : Variable : Type DefaultValue? Directives[Const]?
*/
parseVariableDefinition(): VariableDefinitionNode {
const start = this._lexer.token;
return {
kind: Kind.VARIABLE_DEFINITION,
variable: this.parseVariable(),
type: (this.expectToken(TokenKind.COLON), this.parseTypeReference()),
defaultValue: this.expectOptionalToken(TokenKind.EQUALS)
? this.parseValueLiteral(true)
: undefined,
directives: this.parseDirectives(true),
loc: this.loc(start),
};
}
/**
* Variable : $ Name
*/
parseVariable(): VariableNode {
const start = this._lexer.token;
this.expectToken(TokenKind.DOLLAR);
return {
kind: Kind.VARIABLE,
name: this.parseName(),
loc: this.loc(start),
};
}
/**
* SelectionSet : { Selection+ }
*/
parseSelectionSet(): SelectionSetNode {
const start = this._lexer.token;
return {
kind: Kind.SELECTION_SET,
selections: this.many(
TokenKind.BRACE_L,
this.parseSelection,
TokenKind.BRACE_R,
),
loc: this.loc(start),
};
}
/**
* Selection :
* - Field
* - FragmentSpread
* - InlineFragment
*/
parseSelection(): SelectionNode {
return this.peek(TokenKind.SPREAD)
? this.parseFragment()
: this.parseField();
}
/**
* Field : Alias? Name Arguments? Directives? SelectionSet?
*
* Alias : Name :
*/
parseField(): FieldNode {
const start = this._lexer.token;
const nameOrAlias = this.parseName();
let alias;
let name;
if (this.expectOptionalToken(TokenKind.COLON)) {
alias = nameOrAlias;
name = this.parseName();
} else {
name = nameOrAlias;
}
return {
kind: Kind.FIELD,
alias,
name,
arguments: this.parseArguments(false),
directives: this.parseDirectives(false),
selectionSet: this.peek(TokenKind.BRACE_L)
? this.parseSelectionSet()
: undefined,
loc: this.loc(start),
};
}
/**
* Arguments[Const] : ( Argument[?Const]+ )
*/
parseArguments(isConst: boolean): Array<ArgumentNode> {
const item = isConst ? this.parseConstArgument : this.parseArgument;
return this.optionalMany(TokenKind.PAREN_L, item, TokenKind.PAREN_R);
}
/**
* Argument[Const] : Name : Value[?Const]
*/
parseArgument(): ArgumentNode {
const start = this._lexer.token;
const name = this.parseName();
this.expectToken(TokenKind.COLON);
return {
kind: Kind.ARGUMENT,
name,
value: this.parseValueLiteral(false),
loc: this.loc(start),
};
}
parseConstArgument(): ArgumentNode {
const start = this._lexer.token;
return {
kind: Kind.ARGUMENT,
name: this.parseName(),
value: (this.expectToken(TokenKind.COLON), this.parseValueLiteral(true)),
loc: this.loc(start),
};
}
// Implements the parsing rules in the Fragments section.
/**
* Corresponds to both FragmentSpread and InlineFragment in the spec.
*
* FragmentSpread : ... FragmentName Directives?
*
* InlineFragment : ... TypeCondition? Directives? SelectionSet
*/
parseFragment(): FragmentSpreadNode | InlineFragmentNode {
const start = this._lexer.token;
this.expectToken(TokenKind.SPREAD);
const hasTypeCondition = this.expectOptionalKeyword('on');
if (!hasTypeCondition && this.peek(TokenKind.NAME)) {
return {
kind: Kind.FRAGMENT_SPREAD,
name: this.parseFragmentName(),
directives: this.parseDirectives(false),
loc: this.loc(start),
};
}
return {
kind: Kind.INLINE_FRAGMENT,
typeCondition: hasTypeCondition ? this.parseNamedType() : undefined,
directives: this.parseDirectives(false),
selectionSet: this.parseSelectionSet(),
loc: this.loc(start),
};
}
/**
* FragmentDefinition :
* - fragment FragmentName on TypeCondition Directives? SelectionSet
*
* TypeCondition : NamedType
*/
parseFragmentDefinition(): FragmentDefinitionNode {
const start = this._lexer.token;
this.expectKeyword('fragment');
// Experimental support for defining variables within fragments changes
// the grammar of FragmentDefinition:
// - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
if (this._options?.experimentalFragmentVariables === true) {
return {
kind: Kind.FRAGMENT_DEFINITION,
name: this.parseFragmentName(),
variableDefinitions: this.parseVariableDefinitions(),
typeCondition: (this.expectKeyword('on'), this.parseNamedType()),
directives: this.parseDirectives(false),
selectionSet: this.parseSelectionSet(),
loc: this.loc(start),
};
}
return {
kind: Kind.FRAGMENT_DEFINITION,
name: this.parseFragmentName(),
typeCondition: (this.expectKeyword('on'), this.parseNamedType()),
directives: this.parseDirectives(false),
selectionSet: this.parseSelectionSet(),
loc: this.loc(start),
};
}
/**
* FragmentName : Name but not `on`
*/
parseFragmentName(): NameNode {
if (this._lexer.token.value === 'on') {
throw this.unexpected();
}
return this.parseName();
}
// Implements the parsing rules in the Values section.
/**
* Value[Const] :
* - [~Const] Variable
* - IntValue
* - FloatValue
* - StringValue
* - BooleanValue
* - NullValue
* - EnumValue
* - ListValue[?Const]
* - ObjectValue[?Const]
*
* BooleanValue : one of `true` `false`
*
* NullValue : `null`
*
* EnumValue : Name but not `true`, `false` or `null`
*/
parseValueLiteral(isConst: boolean): ValueNode {
const token = this._lexer.token;
switch (token.kind) {
case TokenKind.BRACKET_L:
return this.parseList(isConst);
case TokenKind.BRACE_L:
return this.parseObject(isConst);
case TokenKind.INT:
this._lexer.advance();
return {
kind: Kind.INT,
value: ((token.value: any): string),
loc: this.loc(token),
};
case TokenKind.FLOAT:
this._lexer.advance();
return {
kind: Kind.FLOAT,
value: ((token.value: any): string),
loc: this.loc(token),
};
case TokenKind.STRING:
case TokenKind.BLOCK_STRING:
return this.parseStringLiteral();
case TokenKind.NAME:
this._lexer.advance();
switch (token.value) {
case 'true':
return { kind: Kind.BOOLEAN, value: true, loc: this.loc(token) };
case 'false':
return { kind: Kind.BOOLEAN, value: false, loc: this.loc(token) };
case 'null':
return { kind: Kind.NULL, loc: this.loc(token) };
default:
return {
kind: Kind.ENUM,
value: ((token.value: any): string),
loc: this.loc(token),
};
}
case TokenKind.DOLLAR:
if (!isConst) {
return this.parseVariable();
}
break;
}
throw this.unexpected();
}
parseStringLiteral(): StringValueNode {
const token = this._lexer.token;
this._lexer.advance();
return {
kind: Kind.STRING,
value: ((token.value: any): string),
block: token.kind === TokenKind.BLOCK_STRING,
loc: this.loc(token),
};
}
/**
* ListValue[Const] :
* - [ ]
* - [ Value[?Const]+ ]
*/
parseList(isConst: boolean): ListValueNode {
const start = this._lexer.token;
const item = () => this.parseValueLiteral(isConst);
return {
kind: Kind.LIST,
values: this.any(TokenKind.BRACKET_L, item, TokenKind.BRACKET_R),
loc: this.loc(start),
};
}
/**
* ObjectValue[Const] :
* - { }
* - { ObjectField[?Const]+ }
*/
parseObject(isConst: boolean): ObjectValueNode {
const start = this._lexer.token;
const item = () => this.parseObjectField(isConst);
return {
kind: Kind.OBJECT,
fields: this.any(TokenKind.BRACE_L, item, TokenKind.BRACE_R),
loc: this.loc(start),
};
}
/**
* ObjectField[Const] : Name : Value[?Const]
*/
parseObjectField(isConst: boolean): ObjectFieldNode {
const start = this._lexer.token;
const name = this.parseName();
this.expectToken(TokenKind.COLON);
return {
kind: Kind.OBJECT_FIELD,
name,
value: this.parseValueLiteral(isConst),
loc: this.loc(start),
};
}
// Implements the parsing rules in the Directives section.
/**
* Directives[Const] : Directive[?Const]+
*/
parseDirectives(isConst: boolean): Array<DirectiveNode> {
const directives = [];
while (this.peek(TokenKind.AT)) {
directives.push(this.parseDirective(isConst));
}
return directives;
}
/**
* Directive[Const] : @ Name Arguments[?Const]?
*/
parseDirective(isConst: boolean): DirectiveNode {
const start = this._lexer.token;
this.expectToken(TokenKind.AT);
return {
kind: Kind.DIRECTIVE,
name: this.parseName(),
arguments: this.parseArguments(isConst),
loc: this.loc(start),
};
}
// Implements the parsing rules in the Types section.
/**
* Type :
* - NamedType
* - ListType
* - NonNullType
*/
parseTypeReference(): TypeNode {
const start = this._lexer.token;
let type;
if (this.expectOptionalToken(TokenKind.BRACKET_L)) {
type = this.parseTypeReference();
this.expectToken(TokenKind.BRACKET_R);
type = {
kind: Kind.LIST_TYPE,
type,
loc: this.loc(start),
};
} else {
type = this.parseNamedType();
}
if (this.expectOptionalToken(TokenKind.BANG)) {
return {
kind: Kind.NON_NULL_TYPE,
type,
loc: this.loc(start),
};
}
return type;
}
/**
* NamedType : Name
*/
parseNamedType(): NamedTypeNode {
const start = this._lexer.token;
return {
kind: Kind.NAMED_TYPE,
name: this.parseName(),
loc: this.loc(start),
};
}
// Implements the parsing rules in the Type Definition section.
/**
* TypeSystemDefinition :
* - SchemaDefinition
* - TypeDefinition
* - DirectiveDefinition
*
* TypeDefinition :
* - ScalarTypeDefinition
* - ObjectTypeDefinition
* - InterfaceTypeDefinition
* - UnionTypeDefinition
* - EnumTypeDefinition
* - InputObjectTypeDefinition
*/
parseTypeSystemDefinition(): TypeSystemDefinitionNode {
// Many definitions begin with a description and require a lookahead.
const keywordToken = this.peekDescription()
? this._lexer.lookahead()
: this._lexer.token;
if (keywordToken.kind === TokenKind.NAME) {
switch (keywordToken.value) {
case 'schema':
return this.parseSchemaDefinition();
case 'scalar':
return this.parseScalarTypeDefinition();
case 'type':
return this.parseObjectTypeDefinition();
case 'interface':
return this.parseInterfaceTypeDefinition();
case 'union':
return this.parseUnionTypeDefinition();
case 'enum':
return this.parseEnumTypeDefinition();
case 'input':
return this.parseInputObjectTypeDefinition();
case 'directive':
return this.parseDirectiveDefinition();
}
}
throw this.unexpected(keywordToken);
}
peekDescription(): boolean {
return this.peek(TokenKind.STRING) || this.peek(TokenKind.BLOCK_STRING);
}
/**
* Description : StringValue
*/
parseDescription(): void | StringValueNode {
if (this.peekDescription()) {
return this.parseStringLiteral();
}
}
/**
* SchemaDefinition : Description? schema Directives[Const]? { OperationTypeDefinition+ }
*/
parseSchemaDefinition(): SchemaDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
this.expectKeyword('schema');
const directives = this.parseDirectives(true);
const operationTypes = this.many(
TokenKind.BRACE_L,
this.parseOperationTypeDefinition,
TokenKind.BRACE_R,
);
return {
kind: Kind.SCHEMA_DEFINITION,
description,
directives,
operationTypes,
loc: this.loc(start),
};
}
/**
* OperationTypeDefinition : OperationType : NamedType
*/
parseOperationTypeDefinition(): OperationTypeDefinitionNode {
const start = this._lexer.token;
const operation = this.parseOperationType();
this.expectToken(TokenKind.COLON);
const type = this.parseNamedType();
return {
kind: Kind.OPERATION_TYPE_DEFINITION,
operation,
type,
loc: this.loc(start),
};
}
/**
* ScalarTypeDefinition : Description? scalar Name Directives[Const]?
*/
parseScalarTypeDefinition(): ScalarTypeDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
this.expectKeyword('scalar');
const name = this.parseName();
const directives = this.parseDirectives(true);
return {
kind: Kind.SCALAR_TYPE_DEFINITION,
description,
name,
directives,
loc: this.loc(start),
};
}
/**
* ObjectTypeDefinition :
* Description?
* type Name ImplementsInterfaces? Directives[Const]? FieldsDefinition?
*/
parseObjectTypeDefinition(): ObjectTypeDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
this.expectKeyword('type');
const name = this.parseName();
const interfaces = this.parseImplementsInterfaces();
const directives = this.parseDirectives(true);
const fields = this.parseFieldsDefinition();
return {
kind: Kind.OBJECT_TYPE_DEFINITION,
description,
name,
interfaces,
directives,
fields,
loc: this.loc(start),
};
}
/**
* ImplementsInterfaces :
* - implements `&`? NamedType
* - ImplementsInterfaces & NamedType
*/
parseImplementsInterfaces(): Array<NamedTypeNode> {
const types = [];
if (this.expectOptionalKeyword('implements')) {
// Optional leading ampersand
this.expectOptionalToken(TokenKind.AMP);
do {
types.push(this.parseNamedType());
} while (
this.expectOptionalToken(TokenKind.AMP) ||
// Legacy support for the SDL?
(this._options?.allowLegacySDLImplementsInterfaces === true &&
this.peek(TokenKind.NAME))
);
}
return types;
}
/**
* FieldsDefinition : { FieldDefinition+ }
*/
parseFieldsDefinition(): Array<FieldDefinitionNode> {
// Legacy support for the SDL?
if (
this._options?.allowLegacySDLEmptyFields === true &&
this.peek(TokenKind.BRACE_L) &&
this._lexer.lookahead().kind === TokenKind.BRACE_R
) {
this._lexer.advance();
this._lexer.advance();
return [];
}
return this.optionalMany(
TokenKind.BRACE_L,
this.parseFieldDefinition,
TokenKind.BRACE_R,
);
}
/**
* FieldDefinition :
* - Description? Name ArgumentsDefinition? : Type Directives[Const]?
*/
parseFieldDefinition(): FieldDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
const name = this.parseName();
const args = this.parseArgumentDefs();
this.expectToken(TokenKind.COLON);
const type = this.parseTypeReference();
const directives = this.parseDirectives(true);
return {
kind: Kind.FIELD_DEFINITION,
description,
name,
arguments: args,
type,
directives,
loc: this.loc(start),
};
}
/**
* ArgumentsDefinition : ( InputValueDefinition+ )
*/
parseArgumentDefs(): Array<InputValueDefinitionNode> {
return this.optionalMany(
TokenKind.PAREN_L,
this.parseInputValueDef,
TokenKind.PAREN_R,
);
}
/**
* InputValueDefinition :
* - Description? Name : Type DefaultValue? Directives[Const]?
*/
parseInputValueDef(): InputValueDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
const name = this.parseName();
this.expectToken(TokenKind.COLON);
const type = this.parseTypeReference();
let defaultValue;
if (this.expectOptionalToken(TokenKind.EQUALS)) {
defaultValue = this.parseValueLiteral(true);
}
const directives = this.parseDirectives(true);
return {
kind: Kind.INPUT_VALUE_DEFINITION,
description,
name,
type,
defaultValue,
directives,
loc: this.loc(start),
};
}
/**
* InterfaceTypeDefinition :
* - Description? interface Name Directives[Const]? FieldsDefinition?
*/
parseInterfaceTypeDefinition(): InterfaceTypeDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
this.expectKeyword('interface');
const name = this.parseName();
const interfaces = this.parseImplementsInterfaces();
const directives = this.parseDirectives(true);
const fields = this.parseFieldsDefinition();
return {
kind: Kind.INTERFACE_TYPE_DEFINITION,
description,
name,
interfaces,
directives,
fields,
loc: this.loc(start),
};
}
/**
* UnionTypeDefinition :
* - Description? union Name Directives[Const]? UnionMemberTypes?
*/
parseUnionTypeDefinition(): UnionTypeDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
this.expectKeyword('union');
const name = this.parseName();
const directives = this.parseDirectives(true);
const types = this.parseUnionMemberTypes();
return {
kind: Kind.UNION_TYPE_DEFINITION,
description,
name,
directives,
types,
loc: this.loc(start),
};
}
/**
* UnionMemberTypes :
* - = `|`? NamedType
* - UnionMemberTypes | NamedType
*/
parseUnionMemberTypes(): Array<NamedTypeNode> {
const types = [];
if (this.expectOptionalToken(TokenKind.EQUALS)) {
// Optional leading pipe
this.expectOptionalToken(TokenKind.PIPE);
do {
types.push(this.parseNamedType());
} while (this.expectOptionalToken(TokenKind.PIPE));
}
return types;
}
/**
* EnumTypeDefinition :
* - Description? enum Name Directives[Const]? EnumValuesDefinition?
*/
parseEnumTypeDefinition(): EnumTypeDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
this.expectKeyword('enum');
const name = this.parseName();
const directives = this.parseDirectives(true);
const values = this.parseEnumValuesDefinition();
return {
kind: Kind.ENUM_TYPE_DEFINITION,
description,
name,
directives,
values,
loc: this.loc(start),
};
}
/**
* EnumValuesDefinition : { EnumValueDefinition+ }
*/
parseEnumValuesDefinition(): Array<EnumValueDefinitionNode> {
return this.optionalMany(
TokenKind.BRACE_L,
this.parseEnumValueDefinition,
TokenKind.BRACE_R,
);
}
/**
* EnumValueDefinition : Description? EnumValue Directives[Const]?
*
* EnumValue : Name
*/
parseEnumValueDefinition(): EnumValueDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
const name = this.parseName();
const directives = this.parseDirectives(true);
return {
kind: Kind.ENUM_VALUE_DEFINITION,
description,
name,
directives,
loc: this.loc(start),
};
}
/**
* InputObjectTypeDefinition :
* - Description? input Name Directives[Const]? InputFieldsDefinition?
*/
parseInputObjectTypeDefinition(): InputObjectTypeDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
this.expectKeyword('input');
const name = this.parseName();
const directives = this.parseDirectives(true);
const fields = this.parseInputFieldsDefinition();
return {
kind: Kind.INPUT_OBJECT_TYPE_DEFINITION,
description,
name,
directives,
fields,
loc: this.loc(start),
};
}
/**
* InputFieldsDefinition : { InputValueDefinition+ }
*/
parseInputFieldsDefinition(): Array<InputValueDefinitionNode> {
return this.optionalMany(
TokenKind.BRACE_L,
this.parseInputValueDef,
TokenKind.BRACE_R,
);
}
/**
* TypeSystemExtension :
* - SchemaExtension
* - TypeExtension
*
* TypeExtension :
* - ScalarTypeExtension
* - ObjectTypeExtension
* - InterfaceTypeExtension
* - UnionTypeExtension
* - EnumTypeExtension
* - InputObjectTypeDefinition
*/
parseTypeSystemExtension(): TypeSystemExtensionNode {
const keywordToken = this._lexer.lookahead();
if (keywordToken.kind === TokenKind.NAME) {
switch (keywordToken.value) {
case 'schema':
return this.parseSchemaExtension();
case 'scalar':
return this.parseScalarTypeExtension();
case 'type':
return this.parseObjectTypeExtension();
case 'interface':
return this.parseInterfaceTypeExtension();
case 'union':
return this.parseUnionTypeExtension();
case 'enum':
return this.parseEnumTypeExtension();
case 'input':
return this.parseInputObjectTypeExtension();
}
}
throw this.unexpected(keywordToken);
}
/**
* SchemaExtension :
* - extend schema Directives[Const]? { OperationTypeDefinition+ }
* - extend schema Directives[Const]
*/
parseSchemaExtension(): SchemaExtensionNode {
const start = this._lexer.token;
this.expectKeyword('extend');
this.expectKeyword('schema');
const directives = this.parseDirectives(true);
const operationTypes = this.optionalMany(
TokenKind.BRACE_L,
this.parseOperationTypeDefinition,
TokenKind.BRACE_R,
);
if (directives.length === 0 && operationTypes.length === 0) {
throw this.unexpected();
}
return {
kind: Kind.SCHEMA_EXTENSION,
directives,
operationTypes,
loc: this.loc(start),
};
}
/**
* ScalarTypeExtension :
* - extend scalar Name Directives[Const]
*/
parseScalarTypeExtension(): ScalarTypeExtensionNode {
const start = this._lexer.token;
this.expectKeyword('extend');
this.expectKeyword('scalar');
const name = this.parseName();
const directives = this.parseDirectives(true);
if (directives.length === 0) {
throw this.unexpected();
}
return {
kind: Kind.SCALAR_TYPE_EXTENSION,
name,
directives,
loc: this.loc(start),
};
}
/**
* ObjectTypeExtension :
* - extend type Name ImplementsInterfaces? Directives[Const]? FieldsDefinition
* - extend type Name ImplementsInterfaces? Directives[Const]
* - extend type Name ImplementsInterfaces
*/
parseObjectTypeExtension(): ObjectTypeExtensionNode {
const start = this._lexer.token;
this.expectKeyword('extend');
this.expectKeyword('type');
const name = this.parseName();
const interfaces = this.parseImplementsInterfaces();
const directives = this.parseDirectives(true);
const fields = this.parseFieldsDefinition();
if (
interfaces.length === 0 &&
directives.length === 0 &&
fields.length === 0
) {
throw this.unexpected();
}
return {
kind: Kind.OBJECT_TYPE_EXTENSION,
name,
interfaces,
directives,
fields,
loc: this.loc(start),
};
}
/**
* InterfaceTypeExtension :
* - extend interface Name ImplementsInterfaces? Directives[Const]? FieldsDefinition
* - extend interface Name ImplementsInterfaces? Directives[Const]
* - extend interface Name ImplementsInterfaces
*/
parseInterfaceTypeExtension(): InterfaceTypeExtensionNode {
const start = this._lexer.token;
this.expectKeyword('extend');
this.expectKeyword('interface');
const name = this.parseName();
const interfaces = this.parseImplementsInterfaces();
const directives = this.parseDirectives(true);
const fields = this.parseFieldsDefinition();
if (
interfaces.length === 0 &&
directives.length === 0 &&
fields.length === 0
) {
throw this.unexpected();
}
return {
kind: Kind.INTERFACE_TYPE_EXTENSION,
name,
interfaces,
directives,
fields,
loc: this.loc(start),
};
}
/**
* UnionTypeExtension :
* - extend union Name Directives[Const]? UnionMemberTypes
* - extend union Name Directives[Const]
*/
parseUnionTypeExtension(): UnionTypeExtensionNode {
const start = this._lexer.token;
this.expectKeyword('extend');
this.expectKeyword('union');
const name = this.parseName();
const directives = this.parseDirectives(true);
const types = this.parseUnionMemberTypes();
if (directives.length === 0 && types.length === 0) {
throw this.unexpected();
}
return {
kind: Kind.UNION_TYPE_EXTENSION,
name,
directives,
types,
loc: this.loc(start),
};
}
/**
* EnumTypeExtension :
* - extend enum Name Directives[Const]? EnumValuesDefinition
* - extend enum Name Directives[Const]
*/
parseEnumTypeExtension(): EnumTypeExtensionNode {
const start = this._lexer.token;
this.expectKeyword('extend');
this.expectKeyword('enum');
const name = this.parseName();
const directives = this.parseDirectives(true);
const values = this.parseEnumValuesDefinition();
if (directives.length === 0 && values.length === 0) {
throw this.unexpected();
}
return {
kind: Kind.ENUM_TYPE_EXTENSION,
name,
directives,
values,
loc: this.loc(start),
};
}
/**
* InputObjectTypeExtension :
* - extend input Name Directives[Const]? InputFieldsDefinition
* - extend input Name Directives[Const]
*/
parseInputObjectTypeExtension(): InputObjectTypeExtensionNode {
const start = this._lexer.token;
this.expectKeyword('extend');
this.expectKeyword('input');
const name = this.parseName();
const directives = this.parseDirectives(true);
const fields = this.parseInputFieldsDefinition();
if (directives.length === 0 && fields.length === 0) {
throw this.unexpected();
}
return {
kind: Kind.INPUT_OBJECT_TYPE_EXTENSION,
name,
directives,
fields,
loc: this.loc(start),
};
}
/**
* DirectiveDefinition :
* - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations
*/
parseDirectiveDefinition(): DirectiveDefinitionNode {
const start = this._lexer.token;
const description = this.parseDescription();
this.expectKeyword('directive');
this.expectToken(TokenKind.AT);
const name = this.parseName();
const args = this.parseArgumentDefs();
const repeatable = this.expectOptionalKeyword('repeatable');
this.expectKeyword('on');
const locations = this.parseDirectiveLocations();
return {
kind: Kind.DIRECTIVE_DEFINITION,
description,
name,
arguments: args,
repeatable,
locations,
loc: this.loc(start),
};
}
/**
* DirectiveLocations :
* - `|`? DirectiveLocation
* - DirectiveLocations | DirectiveLocation
*/
parseDirectiveLocations(): Array<NameNode> {
// Optional leading pipe
this.expectOptionalToken(TokenKind.PIPE);
const locations = [];
do {
locations.push(this.parseDirectiveLocation());
} while (this.expectOptionalToken(TokenKind.PIPE));
return locations;
}
/*
* DirectiveLocation :
* - ExecutableDirectiveLocation
* - TypeSystemDirectiveLocation
*
* ExecutableDirectiveLocation : one of
* `QUERY`
* `MUTATION`
* `SUBSCRIPTION`
* `FIELD`
* `FRAGMENT_DEFINITION`
* `FRAGMENT_SPREAD`
* `INLINE_FRAGMENT`
*
* TypeSystemDirectiveLocation : one of
* `SCHEMA`
* `SCALAR`
* `OBJECT`
* `FIELD_DEFINITION`
* `ARGUMENT_DEFINITION`
* `INTERFACE`
* `UNION`
* `ENUM`
* `ENUM_VALUE`
* `INPUT_OBJECT`
* `INPUT_FIELD_DEFINITION`
*/
parseDirectiveLocation(): NameNode {
const start = this._lexer.token;
const name = this.parseName();
if (DirectiveLocation[name.value] !== undefined) {
return name;
}
throw this.unexpected(start);
}
// Core parsing utility functions
/**
* Returns a location object, used to identify the place in
* the source that created a given parsed object.
*/
loc(startToken: Token): Location | void {
if (this._options?.noLocation !== true) {
return new Location(
startToken,
this._lexer.lastToken,
this._lexer.source,
);
}
}
/**
* Determines if the next token is of a given kind
*/
peek(kind: TokenKindEnum): boolean {
return this._lexer.token.kind === kind;
}
/**
* If the next token is of the given kind, return that token after advancing
* the lexer. Otherwise, do not change the parser state and throw an error.
*/
expectToken(kind: TokenKindEnum): Token {
const token = this._lexer.token;
if (token.kind === kind) {
this._lexer.advance();
return token;
}
throw syntaxError(
this._lexer.source,
token.start,
`Expected ${getTokenKindDesc(kind)}, found ${getTokenDesc(token)}.`,
);
}
/**
* If the next token is of the given kind, return that token after advancing
* the lexer. Otherwise, do not change the parser state and return undefined.
*/
expectOptionalToken(kind: TokenKindEnum): ?Token {
const token = this._lexer.token;
if (token.kind === kind) {
this._lexer.advance();
return token;
}
return undefined;
}
/**
* If the next token is a given keyword, advance the lexer.
* Otherwise, do not change the parser state and throw an error.
*/
expectKeyword(value: string) {
const token = this._lexer.token;
if (token.kind === TokenKind.NAME && token.value === value) {
this._lexer.advance();
} else {
throw syntaxError(
this._lexer.source,
token.start,
`Expected "${value}", found ${getTokenDesc(token)}.`,
);
}
}
/**
* If the next token is a given keyword, return "true" after advancing
* the lexer. Otherwise, do not change the parser state and return "false".
*/
expectOptionalKeyword(value: string): boolean {
const token = this._lexer.token;
if (token.kind === TokenKind.NAME && token.value === value) {
this._lexer.advance();
return true;
}
return false;
}
/**
* Helper function for creating an error when an unexpected lexed token
* is encountered.
*/
unexpected(atToken?: ?Token): GraphQLError {
const token = atToken ?? this._lexer.token;
return syntaxError(
this._lexer.source,
token.start,
`Unexpected ${getTokenDesc(token)}.`,
);
}
/**
* Returns a possibly empty list of parse nodes, determined by
* the parseFn. This list begins with a lex token of openKind
* and ends with a lex token of closeKind. Advances the parser
* to the next lex token after the closing token.
*/
any<T>(
openKind: TokenKindEnum,
parseFn: () => T,
closeKind: TokenKindEnum,
): Array<T> {
this.expectToken(openKind);
const nodes = [];
while (!this.expectOptionalToken(closeKind)) {
nodes.push(parseFn.call(this));
}
return nodes;
}
/**
* Returns a list of parse nodes, determined by the parseFn.
* It can be empty only if open token is missing otherwise it will always
* return non-empty list that begins with a lex token of openKind and ends
* with a lex token of closeKind. Advances the parser to the next lex token
* after the closing token.
*/
optionalMany<T>(
openKind: TokenKindEnum,
parseFn: () => T,
closeKind: TokenKindEnum,
): Array<T> {
if (this.expectOptionalToken(openKind)) {
const nodes = [];
do {
nodes.push(parseFn.call(this));
} while (!this.expectOptionalToken(closeKind));
return nodes;
}
return [];
}
/**
* Returns a non-empty list of parse nodes, determined by
* the parseFn. This list begins with a lex token of openKind
* and ends with a lex token of closeKind. Advances the parser
* to the next lex token after the closing token.
*/
many<T>(
openKind: TokenKindEnum,
parseFn: () => T,
closeKind: TokenKindEnum,
): Array<T> {
this.expectToken(openKind);
const nodes = [];
do {
nodes.push(parseFn.call(this));
} while (!this.expectOptionalToken(closeKind));
return nodes;
}
}
/**
* A helper function to describe a token as a string for debugging
*/
function getTokenDesc(token: Token): string {
const value = token.value;
return getTokenKindDesc(token.kind) + (value != null ? ` "${value}"` : '');
}
/**
* A helper function to describe a token kind as a string for debugging
*/
function getTokenKindDesc(kind: TokenKindEnum): string {
return isPunctuatorTokenKind(kind) ? `"${kind}"` : kind;
}