// @flow strict import type { GraphQLError } from '../error/GraphQLError'; import { syntaxError } from '../error/syntaxError'; import type { TokenKindEnum } from './tokenKind'; import type { Token, NameNode, VariableNode, DocumentNode, DefinitionNode, OperationDefinitionNode, OperationTypeNode, VariableDefinitionNode, SelectionSetNode, SelectionNode, FieldNode, ArgumentNode, FragmentSpreadNode, InlineFragmentNode, FragmentDefinitionNode, ValueNode, StringValueNode, ListValueNode, ObjectValueNode, ObjectFieldNode, DirectiveNode, TypeNode, NamedTypeNode, TypeSystemDefinitionNode, SchemaDefinitionNode, OperationTypeDefinitionNode, ScalarTypeDefinitionNode, ObjectTypeDefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, InterfaceTypeDefinitionNode, UnionTypeDefinitionNode, EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, DirectiveDefinitionNode, TypeSystemExtensionNode, SchemaExtensionNode, ScalarTypeExtensionNode, ObjectTypeExtensionNode, InterfaceTypeExtensionNode, UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, } from './ast'; import { Kind } from './kinds'; import { Location } from './ast'; import { TokenKind } from './tokenKind'; import { Source, isSource } from './source'; import { DirectiveLocation } from './directiveLocation'; import { Lexer, isPunctuatorTokenKind } from './lexer'; /** * 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; } /** * This class is exported only to assist people in implementing their own parsers * without duplicating too much code and should be used only as last resort for cases * such as experimental syntax or if certain features could not be contributed upstream. * * It is still part of the internal API and is versioned, so any changes to it are never * considered breaking changes. If you still need to support multiple versions of the * library, please use the `versionInfo` variable for version detection. * * @internal */ export class Parser { _options: ?ParseOptions; _lexer: Lexer; constructor(source: string | Source, options?: ParseOptions) { const sourceObj = isSource(source) ? source : new Source(source); 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 { 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 { 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 { 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 { if (!this.expectOptionalKeyword('implements')) { return []; } if (this._options?.allowLegacySDLImplementsInterfaces === true) { const types = []; // Optional leading ampersand this.expectOptionalToken(TokenKind.AMP); do { types.push(this.parseNamedType()); } while ( this.expectOptionalToken(TokenKind.AMP) || this.peek(TokenKind.NAME) ); return types; } return this.delimitedMany(TokenKind.AMP, this.parseNamedType); } /** * FieldsDefinition : { FieldDefinition+ } */ parseFieldsDefinition(): Array { // 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 { 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 { return this.expectOptionalToken(TokenKind.EQUALS) ? this.delimitedMany(TokenKind.PIPE, this.parseNamedType) : []; } /** * 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 { 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 { 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 { return this.delimitedMany(TokenKind.PIPE, this.parseDirectiveLocation); } /* * 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( openKind: TokenKindEnum, parseFn: () => T, closeKind: TokenKindEnum, ): Array { 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( openKind: TokenKindEnum, parseFn: () => T, closeKind: TokenKindEnum, ): Array { 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( openKind: TokenKindEnum, parseFn: () => T, closeKind: TokenKindEnum, ): Array { this.expectToken(openKind); const nodes = []; do { nodes.push(parseFn.call(this)); } while (!this.expectOptionalToken(closeKind)); return nodes; } /** * Returns a non-empty list of parse nodes, determined by the parseFn. * This list may begin with a lex token of delimiterKind followed by items separated by lex tokens of tokenKind. * Advances the parser to the next lex token after last item in the list. */ delimitedMany(delimiterKind: TokenKindEnum, parseFn: () => T): Array { this.expectOptionalToken(delimiterKind); const nodes = []; do { nodes.push(parseFn.call(this)); } while (this.expectOptionalToken(delimiterKind)); 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; }