You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1524 lines
38 KiB
1524 lines
38 KiB
import { syntaxError } from '../error/syntaxError.mjs'; |
|
import { Location, OperationTypeNode } from './ast.mjs'; |
|
import { DirectiveLocation } from './directiveLocation.mjs'; |
|
import { Kind } from './kinds.mjs'; |
|
import { isPunctuatorTokenKind, Lexer } from './lexer.mjs'; |
|
import { isSource, Source } from './source.mjs'; |
|
import { TokenKind } from './tokenKind.mjs'; |
|
/** |
|
* Configuration options to control parser behavior |
|
*/ |
|
|
|
/** |
|
* Given a GraphQL source, parses it into a Document. |
|
* Throws GraphQLError if a syntax error is encountered. |
|
*/ |
|
export function parse(source, options) { |
|
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, options) { |
|
const parser = new Parser(source, options); |
|
parser.expectToken(TokenKind.SOF); |
|
const value = parser.parseValueLiteral(false); |
|
parser.expectToken(TokenKind.EOF); |
|
return value; |
|
} |
|
/** |
|
* Similar to parseValue(), but raises a parse error if it encounters a |
|
* variable. The return type will be a constant value. |
|
*/ |
|
|
|
export function parseConstValue(source, options) { |
|
const parser = new Parser(source, options); |
|
parser.expectToken(TokenKind.SOF); |
|
const value = parser.parseConstValueLiteral(); |
|
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, options) { |
|
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 { |
|
constructor(source, options = {}) { |
|
const sourceObj = isSource(source) ? source : new Source(source); |
|
this._lexer = new Lexer(sourceObj); |
|
this._options = options; |
|
this._tokenCounter = 0; |
|
} |
|
/** |
|
* Converts a name lex token into a name parse node. |
|
*/ |
|
|
|
parseName() { |
|
const token = this.expectToken(TokenKind.NAME); |
|
return this.node(token, { |
|
kind: Kind.NAME, |
|
value: token.value, |
|
}); |
|
} // Implements the parsing rules in the Document section. |
|
|
|
/** |
|
* Document : Definition+ |
|
*/ |
|
|
|
parseDocument() { |
|
return this.node(this._lexer.token, { |
|
kind: Kind.DOCUMENT, |
|
definitions: this.many( |
|
TokenKind.SOF, |
|
this.parseDefinition, |
|
TokenKind.EOF, |
|
), |
|
}); |
|
} |
|
/** |
|
* Definition : |
|
* - ExecutableDefinition |
|
* - TypeSystemDefinition |
|
* - TypeSystemExtension |
|
* |
|
* ExecutableDefinition : |
|
* - OperationDefinition |
|
* - FragmentDefinition |
|
* |
|
* TypeSystemDefinition : |
|
* - SchemaDefinition |
|
* - TypeDefinition |
|
* - DirectiveDefinition |
|
* |
|
* TypeDefinition : |
|
* - ScalarTypeDefinition |
|
* - ObjectTypeDefinition |
|
* - InterfaceTypeDefinition |
|
* - UnionTypeDefinition |
|
* - EnumTypeDefinition |
|
* - InputObjectTypeDefinition |
|
*/ |
|
|
|
parseDefinition() { |
|
if (this.peek(TokenKind.BRACE_L)) { |
|
return this.parseOperationDefinition(); |
|
} // Many definitions begin with a description and require a lookahead. |
|
|
|
const hasDescription = this.peekDescription(); |
|
const keywordToken = hasDescription |
|
? 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(); |
|
} |
|
|
|
if (hasDescription) { |
|
throw syntaxError( |
|
this._lexer.source, |
|
this._lexer.token.start, |
|
'Unexpected description, descriptions are supported only on type definitions.', |
|
); |
|
} |
|
|
|
switch (keywordToken.value) { |
|
case 'query': |
|
case 'mutation': |
|
case 'subscription': |
|
return this.parseOperationDefinition(); |
|
|
|
case 'fragment': |
|
return this.parseFragmentDefinition(); |
|
|
|
case 'extend': |
|
return this.parseTypeSystemExtension(); |
|
} |
|
} |
|
|
|
throw this.unexpected(keywordToken); |
|
} // Implements the parsing rules in the Operations section. |
|
|
|
/** |
|
* OperationDefinition : |
|
* - SelectionSet |
|
* - OperationType Name? VariableDefinitions? Directives? SelectionSet |
|
*/ |
|
|
|
parseOperationDefinition() { |
|
const start = this._lexer.token; |
|
|
|
if (this.peek(TokenKind.BRACE_L)) { |
|
return this.node(start, { |
|
kind: Kind.OPERATION_DEFINITION, |
|
operation: OperationTypeNode.QUERY, |
|
name: undefined, |
|
variableDefinitions: [], |
|
directives: [], |
|
selectionSet: this.parseSelectionSet(), |
|
}); |
|
} |
|
|
|
const operation = this.parseOperationType(); |
|
let name; |
|
|
|
if (this.peek(TokenKind.NAME)) { |
|
name = this.parseName(); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.OPERATION_DEFINITION, |
|
operation, |
|
name, |
|
variableDefinitions: this.parseVariableDefinitions(), |
|
directives: this.parseDirectives(false), |
|
selectionSet: this.parseSelectionSet(), |
|
}); |
|
} |
|
/** |
|
* OperationType : one of query mutation subscription |
|
*/ |
|
|
|
parseOperationType() { |
|
const operationToken = this.expectToken(TokenKind.NAME); |
|
|
|
switch (operationToken.value) { |
|
case 'query': |
|
return OperationTypeNode.QUERY; |
|
|
|
case 'mutation': |
|
return OperationTypeNode.MUTATION; |
|
|
|
case 'subscription': |
|
return OperationTypeNode.SUBSCRIPTION; |
|
} |
|
|
|
throw this.unexpected(operationToken); |
|
} |
|
/** |
|
* VariableDefinitions : ( VariableDefinition+ ) |
|
*/ |
|
|
|
parseVariableDefinitions() { |
|
return this.optionalMany( |
|
TokenKind.PAREN_L, |
|
this.parseVariableDefinition, |
|
TokenKind.PAREN_R, |
|
); |
|
} |
|
/** |
|
* VariableDefinition : Variable : Type DefaultValue? Directives[Const]? |
|
*/ |
|
|
|
parseVariableDefinition() { |
|
return this.node(this._lexer.token, { |
|
kind: Kind.VARIABLE_DEFINITION, |
|
variable: this.parseVariable(), |
|
type: (this.expectToken(TokenKind.COLON), this.parseTypeReference()), |
|
defaultValue: this.expectOptionalToken(TokenKind.EQUALS) |
|
? this.parseConstValueLiteral() |
|
: undefined, |
|
directives: this.parseConstDirectives(), |
|
}); |
|
} |
|
/** |
|
* Variable : $ Name |
|
*/ |
|
|
|
parseVariable() { |
|
const start = this._lexer.token; |
|
this.expectToken(TokenKind.DOLLAR); |
|
return this.node(start, { |
|
kind: Kind.VARIABLE, |
|
name: this.parseName(), |
|
}); |
|
} |
|
/** |
|
* ``` |
|
* SelectionSet : { Selection+ } |
|
* ``` |
|
*/ |
|
|
|
parseSelectionSet() { |
|
return this.node(this._lexer.token, { |
|
kind: Kind.SELECTION_SET, |
|
selections: this.many( |
|
TokenKind.BRACE_L, |
|
this.parseSelection, |
|
TokenKind.BRACE_R, |
|
), |
|
}); |
|
} |
|
/** |
|
* Selection : |
|
* - Field |
|
* - FragmentSpread |
|
* - InlineFragment |
|
*/ |
|
|
|
parseSelection() { |
|
return this.peek(TokenKind.SPREAD) |
|
? this.parseFragment() |
|
: this.parseField(); |
|
} |
|
/** |
|
* Field : Alias? Name Arguments? Directives? SelectionSet? |
|
* |
|
* Alias : Name : |
|
*/ |
|
|
|
parseField() { |
|
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 this.node(start, { |
|
kind: Kind.FIELD, |
|
alias, |
|
name, |
|
arguments: this.parseArguments(false), |
|
directives: this.parseDirectives(false), |
|
selectionSet: this.peek(TokenKind.BRACE_L) |
|
? this.parseSelectionSet() |
|
: undefined, |
|
}); |
|
} |
|
/** |
|
* Arguments[Const] : ( Argument[?Const]+ ) |
|
*/ |
|
|
|
parseArguments(isConst) { |
|
const item = isConst ? this.parseConstArgument : this.parseArgument; |
|
return this.optionalMany(TokenKind.PAREN_L, item, TokenKind.PAREN_R); |
|
} |
|
/** |
|
* Argument[Const] : Name : Value[?Const] |
|
*/ |
|
|
|
parseArgument(isConst = false) { |
|
const start = this._lexer.token; |
|
const name = this.parseName(); |
|
this.expectToken(TokenKind.COLON); |
|
return this.node(start, { |
|
kind: Kind.ARGUMENT, |
|
name, |
|
value: this.parseValueLiteral(isConst), |
|
}); |
|
} |
|
|
|
parseConstArgument() { |
|
return this.parseArgument(true); |
|
} // 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() { |
|
const start = this._lexer.token; |
|
this.expectToken(TokenKind.SPREAD); |
|
const hasTypeCondition = this.expectOptionalKeyword('on'); |
|
|
|
if (!hasTypeCondition && this.peek(TokenKind.NAME)) { |
|
return this.node(start, { |
|
kind: Kind.FRAGMENT_SPREAD, |
|
name: this.parseFragmentName(), |
|
directives: this.parseDirectives(false), |
|
}); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.INLINE_FRAGMENT, |
|
typeCondition: hasTypeCondition ? this.parseNamedType() : undefined, |
|
directives: this.parseDirectives(false), |
|
selectionSet: this.parseSelectionSet(), |
|
}); |
|
} |
|
/** |
|
* FragmentDefinition : |
|
* - fragment FragmentName on TypeCondition Directives? SelectionSet |
|
* |
|
* TypeCondition : NamedType |
|
*/ |
|
|
|
parseFragmentDefinition() { |
|
const start = this._lexer.token; |
|
this.expectKeyword('fragment'); // Legacy support for defining variables within fragments changes |
|
// the grammar of FragmentDefinition: |
|
// - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet |
|
|
|
if (this._options.allowLegacyFragmentVariables === true) { |
|
return this.node(start, { |
|
kind: Kind.FRAGMENT_DEFINITION, |
|
name: this.parseFragmentName(), |
|
variableDefinitions: this.parseVariableDefinitions(), |
|
typeCondition: (this.expectKeyword('on'), this.parseNamedType()), |
|
directives: this.parseDirectives(false), |
|
selectionSet: this.parseSelectionSet(), |
|
}); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.FRAGMENT_DEFINITION, |
|
name: this.parseFragmentName(), |
|
typeCondition: (this.expectKeyword('on'), this.parseNamedType()), |
|
directives: this.parseDirectives(false), |
|
selectionSet: this.parseSelectionSet(), |
|
}); |
|
} |
|
/** |
|
* FragmentName : Name but not `on` |
|
*/ |
|
|
|
parseFragmentName() { |
|
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) { |
|
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.advanceLexer(); |
|
return this.node(token, { |
|
kind: Kind.INT, |
|
value: token.value, |
|
}); |
|
|
|
case TokenKind.FLOAT: |
|
this.advanceLexer(); |
|
return this.node(token, { |
|
kind: Kind.FLOAT, |
|
value: token.value, |
|
}); |
|
|
|
case TokenKind.STRING: |
|
case TokenKind.BLOCK_STRING: |
|
return this.parseStringLiteral(); |
|
|
|
case TokenKind.NAME: |
|
this.advanceLexer(); |
|
|
|
switch (token.value) { |
|
case 'true': |
|
return this.node(token, { |
|
kind: Kind.BOOLEAN, |
|
value: true, |
|
}); |
|
|
|
case 'false': |
|
return this.node(token, { |
|
kind: Kind.BOOLEAN, |
|
value: false, |
|
}); |
|
|
|
case 'null': |
|
return this.node(token, { |
|
kind: Kind.NULL, |
|
}); |
|
|
|
default: |
|
return this.node(token, { |
|
kind: Kind.ENUM, |
|
value: token.value, |
|
}); |
|
} |
|
|
|
case TokenKind.DOLLAR: |
|
if (isConst) { |
|
this.expectToken(TokenKind.DOLLAR); |
|
|
|
if (this._lexer.token.kind === TokenKind.NAME) { |
|
const varName = this._lexer.token.value; |
|
throw syntaxError( |
|
this._lexer.source, |
|
token.start, |
|
`Unexpected variable "$${varName}" in constant value.`, |
|
); |
|
} else { |
|
throw this.unexpected(token); |
|
} |
|
} |
|
|
|
return this.parseVariable(); |
|
|
|
default: |
|
throw this.unexpected(); |
|
} |
|
} |
|
|
|
parseConstValueLiteral() { |
|
return this.parseValueLiteral(true); |
|
} |
|
|
|
parseStringLiteral() { |
|
const token = this._lexer.token; |
|
this.advanceLexer(); |
|
return this.node(token, { |
|
kind: Kind.STRING, |
|
value: token.value, |
|
block: token.kind === TokenKind.BLOCK_STRING, |
|
}); |
|
} |
|
/** |
|
* ListValue[Const] : |
|
* - [ ] |
|
* - [ Value[?Const]+ ] |
|
*/ |
|
|
|
parseList(isConst) { |
|
const item = () => this.parseValueLiteral(isConst); |
|
|
|
return this.node(this._lexer.token, { |
|
kind: Kind.LIST, |
|
values: this.any(TokenKind.BRACKET_L, item, TokenKind.BRACKET_R), |
|
}); |
|
} |
|
/** |
|
* ``` |
|
* ObjectValue[Const] : |
|
* - { } |
|
* - { ObjectField[?Const]+ } |
|
* ``` |
|
*/ |
|
|
|
parseObject(isConst) { |
|
const item = () => this.parseObjectField(isConst); |
|
|
|
return this.node(this._lexer.token, { |
|
kind: Kind.OBJECT, |
|
fields: this.any(TokenKind.BRACE_L, item, TokenKind.BRACE_R), |
|
}); |
|
} |
|
/** |
|
* ObjectField[Const] : Name : Value[?Const] |
|
*/ |
|
|
|
parseObjectField(isConst) { |
|
const start = this._lexer.token; |
|
const name = this.parseName(); |
|
this.expectToken(TokenKind.COLON); |
|
return this.node(start, { |
|
kind: Kind.OBJECT_FIELD, |
|
name, |
|
value: this.parseValueLiteral(isConst), |
|
}); |
|
} // Implements the parsing rules in the Directives section. |
|
|
|
/** |
|
* Directives[Const] : Directive[?Const]+ |
|
*/ |
|
|
|
parseDirectives(isConst) { |
|
const directives = []; |
|
|
|
while (this.peek(TokenKind.AT)) { |
|
directives.push(this.parseDirective(isConst)); |
|
} |
|
|
|
return directives; |
|
} |
|
|
|
parseConstDirectives() { |
|
return this.parseDirectives(true); |
|
} |
|
/** |
|
* ``` |
|
* Directive[Const] : @ Name Arguments[?Const]? |
|
* ``` |
|
*/ |
|
|
|
parseDirective(isConst) { |
|
const start = this._lexer.token; |
|
this.expectToken(TokenKind.AT); |
|
return this.node(start, { |
|
kind: Kind.DIRECTIVE, |
|
name: this.parseName(), |
|
arguments: this.parseArguments(isConst), |
|
}); |
|
} // Implements the parsing rules in the Types section. |
|
|
|
/** |
|
* Type : |
|
* - NamedType |
|
* - ListType |
|
* - NonNullType |
|
*/ |
|
|
|
parseTypeReference() { |
|
const start = this._lexer.token; |
|
let type; |
|
|
|
if (this.expectOptionalToken(TokenKind.BRACKET_L)) { |
|
const innerType = this.parseTypeReference(); |
|
this.expectToken(TokenKind.BRACKET_R); |
|
type = this.node(start, { |
|
kind: Kind.LIST_TYPE, |
|
type: innerType, |
|
}); |
|
} else { |
|
type = this.parseNamedType(); |
|
} |
|
|
|
if (this.expectOptionalToken(TokenKind.BANG)) { |
|
return this.node(start, { |
|
kind: Kind.NON_NULL_TYPE, |
|
type, |
|
}); |
|
} |
|
|
|
return type; |
|
} |
|
/** |
|
* NamedType : Name |
|
*/ |
|
|
|
parseNamedType() { |
|
return this.node(this._lexer.token, { |
|
kind: Kind.NAMED_TYPE, |
|
name: this.parseName(), |
|
}); |
|
} // Implements the parsing rules in the Type Definition section. |
|
|
|
peekDescription() { |
|
return this.peek(TokenKind.STRING) || this.peek(TokenKind.BLOCK_STRING); |
|
} |
|
/** |
|
* Description : StringValue |
|
*/ |
|
|
|
parseDescription() { |
|
if (this.peekDescription()) { |
|
return this.parseStringLiteral(); |
|
} |
|
} |
|
/** |
|
* ``` |
|
* SchemaDefinition : Description? schema Directives[Const]? { OperationTypeDefinition+ } |
|
* ``` |
|
*/ |
|
|
|
parseSchemaDefinition() { |
|
const start = this._lexer.token; |
|
const description = this.parseDescription(); |
|
this.expectKeyword('schema'); |
|
const directives = this.parseConstDirectives(); |
|
const operationTypes = this.many( |
|
TokenKind.BRACE_L, |
|
this.parseOperationTypeDefinition, |
|
TokenKind.BRACE_R, |
|
); |
|
return this.node(start, { |
|
kind: Kind.SCHEMA_DEFINITION, |
|
description, |
|
directives, |
|
operationTypes, |
|
}); |
|
} |
|
/** |
|
* OperationTypeDefinition : OperationType : NamedType |
|
*/ |
|
|
|
parseOperationTypeDefinition() { |
|
const start = this._lexer.token; |
|
const operation = this.parseOperationType(); |
|
this.expectToken(TokenKind.COLON); |
|
const type = this.parseNamedType(); |
|
return this.node(start, { |
|
kind: Kind.OPERATION_TYPE_DEFINITION, |
|
operation, |
|
type, |
|
}); |
|
} |
|
/** |
|
* ScalarTypeDefinition : Description? scalar Name Directives[Const]? |
|
*/ |
|
|
|
parseScalarTypeDefinition() { |
|
const start = this._lexer.token; |
|
const description = this.parseDescription(); |
|
this.expectKeyword('scalar'); |
|
const name = this.parseName(); |
|
const directives = this.parseConstDirectives(); |
|
return this.node(start, { |
|
kind: Kind.SCALAR_TYPE_DEFINITION, |
|
description, |
|
name, |
|
directives, |
|
}); |
|
} |
|
/** |
|
* ObjectTypeDefinition : |
|
* Description? |
|
* type Name ImplementsInterfaces? Directives[Const]? FieldsDefinition? |
|
*/ |
|
|
|
parseObjectTypeDefinition() { |
|
const start = this._lexer.token; |
|
const description = this.parseDescription(); |
|
this.expectKeyword('type'); |
|
const name = this.parseName(); |
|
const interfaces = this.parseImplementsInterfaces(); |
|
const directives = this.parseConstDirectives(); |
|
const fields = this.parseFieldsDefinition(); |
|
return this.node(start, { |
|
kind: Kind.OBJECT_TYPE_DEFINITION, |
|
description, |
|
name, |
|
interfaces, |
|
directives, |
|
fields, |
|
}); |
|
} |
|
/** |
|
* ImplementsInterfaces : |
|
* - implements `&`? NamedType |
|
* - ImplementsInterfaces & NamedType |
|
*/ |
|
|
|
parseImplementsInterfaces() { |
|
return this.expectOptionalKeyword('implements') |
|
? this.delimitedMany(TokenKind.AMP, this.parseNamedType) |
|
: []; |
|
} |
|
/** |
|
* ``` |
|
* FieldsDefinition : { FieldDefinition+ } |
|
* ``` |
|
*/ |
|
|
|
parseFieldsDefinition() { |
|
return this.optionalMany( |
|
TokenKind.BRACE_L, |
|
this.parseFieldDefinition, |
|
TokenKind.BRACE_R, |
|
); |
|
} |
|
/** |
|
* FieldDefinition : |
|
* - Description? Name ArgumentsDefinition? : Type Directives[Const]? |
|
*/ |
|
|
|
parseFieldDefinition() { |
|
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.parseConstDirectives(); |
|
return this.node(start, { |
|
kind: Kind.FIELD_DEFINITION, |
|
description, |
|
name, |
|
arguments: args, |
|
type, |
|
directives, |
|
}); |
|
} |
|
/** |
|
* ArgumentsDefinition : ( InputValueDefinition+ ) |
|
*/ |
|
|
|
parseArgumentDefs() { |
|
return this.optionalMany( |
|
TokenKind.PAREN_L, |
|
this.parseInputValueDef, |
|
TokenKind.PAREN_R, |
|
); |
|
} |
|
/** |
|
* InputValueDefinition : |
|
* - Description? Name : Type DefaultValue? Directives[Const]? |
|
*/ |
|
|
|
parseInputValueDef() { |
|
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.parseConstValueLiteral(); |
|
} |
|
|
|
const directives = this.parseConstDirectives(); |
|
return this.node(start, { |
|
kind: Kind.INPUT_VALUE_DEFINITION, |
|
description, |
|
name, |
|
type, |
|
defaultValue, |
|
directives, |
|
}); |
|
} |
|
/** |
|
* InterfaceTypeDefinition : |
|
* - Description? interface Name Directives[Const]? FieldsDefinition? |
|
*/ |
|
|
|
parseInterfaceTypeDefinition() { |
|
const start = this._lexer.token; |
|
const description = this.parseDescription(); |
|
this.expectKeyword('interface'); |
|
const name = this.parseName(); |
|
const interfaces = this.parseImplementsInterfaces(); |
|
const directives = this.parseConstDirectives(); |
|
const fields = this.parseFieldsDefinition(); |
|
return this.node(start, { |
|
kind: Kind.INTERFACE_TYPE_DEFINITION, |
|
description, |
|
name, |
|
interfaces, |
|
directives, |
|
fields, |
|
}); |
|
} |
|
/** |
|
* UnionTypeDefinition : |
|
* - Description? union Name Directives[Const]? UnionMemberTypes? |
|
*/ |
|
|
|
parseUnionTypeDefinition() { |
|
const start = this._lexer.token; |
|
const description = this.parseDescription(); |
|
this.expectKeyword('union'); |
|
const name = this.parseName(); |
|
const directives = this.parseConstDirectives(); |
|
const types = this.parseUnionMemberTypes(); |
|
return this.node(start, { |
|
kind: Kind.UNION_TYPE_DEFINITION, |
|
description, |
|
name, |
|
directives, |
|
types, |
|
}); |
|
} |
|
/** |
|
* UnionMemberTypes : |
|
* - = `|`? NamedType |
|
* - UnionMemberTypes | NamedType |
|
*/ |
|
|
|
parseUnionMemberTypes() { |
|
return this.expectOptionalToken(TokenKind.EQUALS) |
|
? this.delimitedMany(TokenKind.PIPE, this.parseNamedType) |
|
: []; |
|
} |
|
/** |
|
* EnumTypeDefinition : |
|
* - Description? enum Name Directives[Const]? EnumValuesDefinition? |
|
*/ |
|
|
|
parseEnumTypeDefinition() { |
|
const start = this._lexer.token; |
|
const description = this.parseDescription(); |
|
this.expectKeyword('enum'); |
|
const name = this.parseName(); |
|
const directives = this.parseConstDirectives(); |
|
const values = this.parseEnumValuesDefinition(); |
|
return this.node(start, { |
|
kind: Kind.ENUM_TYPE_DEFINITION, |
|
description, |
|
name, |
|
directives, |
|
values, |
|
}); |
|
} |
|
/** |
|
* ``` |
|
* EnumValuesDefinition : { EnumValueDefinition+ } |
|
* ``` |
|
*/ |
|
|
|
parseEnumValuesDefinition() { |
|
return this.optionalMany( |
|
TokenKind.BRACE_L, |
|
this.parseEnumValueDefinition, |
|
TokenKind.BRACE_R, |
|
); |
|
} |
|
/** |
|
* EnumValueDefinition : Description? EnumValue Directives[Const]? |
|
*/ |
|
|
|
parseEnumValueDefinition() { |
|
const start = this._lexer.token; |
|
const description = this.parseDescription(); |
|
const name = this.parseEnumValueName(); |
|
const directives = this.parseConstDirectives(); |
|
return this.node(start, { |
|
kind: Kind.ENUM_VALUE_DEFINITION, |
|
description, |
|
name, |
|
directives, |
|
}); |
|
} |
|
/** |
|
* EnumValue : Name but not `true`, `false` or `null` |
|
*/ |
|
|
|
parseEnumValueName() { |
|
if ( |
|
this._lexer.token.value === 'true' || |
|
this._lexer.token.value === 'false' || |
|
this._lexer.token.value === 'null' |
|
) { |
|
throw syntaxError( |
|
this._lexer.source, |
|
this._lexer.token.start, |
|
`${getTokenDesc( |
|
this._lexer.token, |
|
)} is reserved and cannot be used for an enum value.`, |
|
); |
|
} |
|
|
|
return this.parseName(); |
|
} |
|
/** |
|
* InputObjectTypeDefinition : |
|
* - Description? input Name Directives[Const]? InputFieldsDefinition? |
|
*/ |
|
|
|
parseInputObjectTypeDefinition() { |
|
const start = this._lexer.token; |
|
const description = this.parseDescription(); |
|
this.expectKeyword('input'); |
|
const name = this.parseName(); |
|
const directives = this.parseConstDirectives(); |
|
const fields = this.parseInputFieldsDefinition(); |
|
return this.node(start, { |
|
kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, |
|
description, |
|
name, |
|
directives, |
|
fields, |
|
}); |
|
} |
|
/** |
|
* ``` |
|
* InputFieldsDefinition : { InputValueDefinition+ } |
|
* ``` |
|
*/ |
|
|
|
parseInputFieldsDefinition() { |
|
return this.optionalMany( |
|
TokenKind.BRACE_L, |
|
this.parseInputValueDef, |
|
TokenKind.BRACE_R, |
|
); |
|
} |
|
/** |
|
* TypeSystemExtension : |
|
* - SchemaExtension |
|
* - TypeExtension |
|
* |
|
* TypeExtension : |
|
* - ScalarTypeExtension |
|
* - ObjectTypeExtension |
|
* - InterfaceTypeExtension |
|
* - UnionTypeExtension |
|
* - EnumTypeExtension |
|
* - InputObjectTypeDefinition |
|
*/ |
|
|
|
parseTypeSystemExtension() { |
|
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() { |
|
const start = this._lexer.token; |
|
this.expectKeyword('extend'); |
|
this.expectKeyword('schema'); |
|
const directives = this.parseConstDirectives(); |
|
const operationTypes = this.optionalMany( |
|
TokenKind.BRACE_L, |
|
this.parseOperationTypeDefinition, |
|
TokenKind.BRACE_R, |
|
); |
|
|
|
if (directives.length === 0 && operationTypes.length === 0) { |
|
throw this.unexpected(); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.SCHEMA_EXTENSION, |
|
directives, |
|
operationTypes, |
|
}); |
|
} |
|
/** |
|
* ScalarTypeExtension : |
|
* - extend scalar Name Directives[Const] |
|
*/ |
|
|
|
parseScalarTypeExtension() { |
|
const start = this._lexer.token; |
|
this.expectKeyword('extend'); |
|
this.expectKeyword('scalar'); |
|
const name = this.parseName(); |
|
const directives = this.parseConstDirectives(); |
|
|
|
if (directives.length === 0) { |
|
throw this.unexpected(); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.SCALAR_TYPE_EXTENSION, |
|
name, |
|
directives, |
|
}); |
|
} |
|
/** |
|
* ObjectTypeExtension : |
|
* - extend type Name ImplementsInterfaces? Directives[Const]? FieldsDefinition |
|
* - extend type Name ImplementsInterfaces? Directives[Const] |
|
* - extend type Name ImplementsInterfaces |
|
*/ |
|
|
|
parseObjectTypeExtension() { |
|
const start = this._lexer.token; |
|
this.expectKeyword('extend'); |
|
this.expectKeyword('type'); |
|
const name = this.parseName(); |
|
const interfaces = this.parseImplementsInterfaces(); |
|
const directives = this.parseConstDirectives(); |
|
const fields = this.parseFieldsDefinition(); |
|
|
|
if ( |
|
interfaces.length === 0 && |
|
directives.length === 0 && |
|
fields.length === 0 |
|
) { |
|
throw this.unexpected(); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.OBJECT_TYPE_EXTENSION, |
|
name, |
|
interfaces, |
|
directives, |
|
fields, |
|
}); |
|
} |
|
/** |
|
* InterfaceTypeExtension : |
|
* - extend interface Name ImplementsInterfaces? Directives[Const]? FieldsDefinition |
|
* - extend interface Name ImplementsInterfaces? Directives[Const] |
|
* - extend interface Name ImplementsInterfaces |
|
*/ |
|
|
|
parseInterfaceTypeExtension() { |
|
const start = this._lexer.token; |
|
this.expectKeyword('extend'); |
|
this.expectKeyword('interface'); |
|
const name = this.parseName(); |
|
const interfaces = this.parseImplementsInterfaces(); |
|
const directives = this.parseConstDirectives(); |
|
const fields = this.parseFieldsDefinition(); |
|
|
|
if ( |
|
interfaces.length === 0 && |
|
directives.length === 0 && |
|
fields.length === 0 |
|
) { |
|
throw this.unexpected(); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.INTERFACE_TYPE_EXTENSION, |
|
name, |
|
interfaces, |
|
directives, |
|
fields, |
|
}); |
|
} |
|
/** |
|
* UnionTypeExtension : |
|
* - extend union Name Directives[Const]? UnionMemberTypes |
|
* - extend union Name Directives[Const] |
|
*/ |
|
|
|
parseUnionTypeExtension() { |
|
const start = this._lexer.token; |
|
this.expectKeyword('extend'); |
|
this.expectKeyword('union'); |
|
const name = this.parseName(); |
|
const directives = this.parseConstDirectives(); |
|
const types = this.parseUnionMemberTypes(); |
|
|
|
if (directives.length === 0 && types.length === 0) { |
|
throw this.unexpected(); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.UNION_TYPE_EXTENSION, |
|
name, |
|
directives, |
|
types, |
|
}); |
|
} |
|
/** |
|
* EnumTypeExtension : |
|
* - extend enum Name Directives[Const]? EnumValuesDefinition |
|
* - extend enum Name Directives[Const] |
|
*/ |
|
|
|
parseEnumTypeExtension() { |
|
const start = this._lexer.token; |
|
this.expectKeyword('extend'); |
|
this.expectKeyword('enum'); |
|
const name = this.parseName(); |
|
const directives = this.parseConstDirectives(); |
|
const values = this.parseEnumValuesDefinition(); |
|
|
|
if (directives.length === 0 && values.length === 0) { |
|
throw this.unexpected(); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.ENUM_TYPE_EXTENSION, |
|
name, |
|
directives, |
|
values, |
|
}); |
|
} |
|
/** |
|
* InputObjectTypeExtension : |
|
* - extend input Name Directives[Const]? InputFieldsDefinition |
|
* - extend input Name Directives[Const] |
|
*/ |
|
|
|
parseInputObjectTypeExtension() { |
|
const start = this._lexer.token; |
|
this.expectKeyword('extend'); |
|
this.expectKeyword('input'); |
|
const name = this.parseName(); |
|
const directives = this.parseConstDirectives(); |
|
const fields = this.parseInputFieldsDefinition(); |
|
|
|
if (directives.length === 0 && fields.length === 0) { |
|
throw this.unexpected(); |
|
} |
|
|
|
return this.node(start, { |
|
kind: Kind.INPUT_OBJECT_TYPE_EXTENSION, |
|
name, |
|
directives, |
|
fields, |
|
}); |
|
} |
|
/** |
|
* ``` |
|
* DirectiveDefinition : |
|
* - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations |
|
* ``` |
|
*/ |
|
|
|
parseDirectiveDefinition() { |
|
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 this.node(start, { |
|
kind: Kind.DIRECTIVE_DEFINITION, |
|
description, |
|
name, |
|
arguments: args, |
|
repeatable, |
|
locations, |
|
}); |
|
} |
|
/** |
|
* DirectiveLocations : |
|
* - `|`? DirectiveLocation |
|
* - DirectiveLocations | DirectiveLocation |
|
*/ |
|
|
|
parseDirectiveLocations() { |
|
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() { |
|
const start = this._lexer.token; |
|
const name = this.parseName(); |
|
|
|
if (Object.prototype.hasOwnProperty.call(DirectiveLocation, name.value)) { |
|
return name; |
|
} |
|
|
|
throw this.unexpected(start); |
|
} // Core parsing utility functions |
|
|
|
/** |
|
* Returns a node that, if configured to do so, sets a "loc" field as a |
|
* location object, used to identify the place in the source that created a |
|
* given parsed object. |
|
*/ |
|
|
|
node(startToken, node) { |
|
if (this._options.noLocation !== true) { |
|
node.loc = new Location( |
|
startToken, |
|
this._lexer.lastToken, |
|
this._lexer.source, |
|
); |
|
} |
|
|
|
return node; |
|
} |
|
/** |
|
* Determines if the next token is of a given kind |
|
*/ |
|
|
|
peek(kind) { |
|
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) { |
|
const token = this._lexer.token; |
|
|
|
if (token.kind === kind) { |
|
this.advanceLexer(); |
|
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 "true" after advancing the lexer. |
|
* Otherwise, do not change the parser state and return "false". |
|
*/ |
|
|
|
expectOptionalToken(kind) { |
|
const token = this._lexer.token; |
|
|
|
if (token.kind === kind) { |
|
this.advanceLexer(); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
/** |
|
* If the next token is a given keyword, advance the lexer. |
|
* Otherwise, do not change the parser state and throw an error. |
|
*/ |
|
|
|
expectKeyword(value) { |
|
const token = this._lexer.token; |
|
|
|
if (token.kind === TokenKind.NAME && token.value === value) { |
|
this.advanceLexer(); |
|
} 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) { |
|
const token = this._lexer.token; |
|
|
|
if (token.kind === TokenKind.NAME && token.value === value) { |
|
this.advanceLexer(); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
/** |
|
* Helper function for creating an error when an unexpected lexed token is encountered. |
|
*/ |
|
|
|
unexpected(atToken) { |
|
const token = |
|
atToken !== null && atToken !== void 0 ? 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, parseFn, closeKind) { |
|
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, parseFn, closeKind) { |
|
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, parseFn, closeKind) { |
|
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, parseFn) { |
|
this.expectOptionalToken(delimiterKind); |
|
const nodes = []; |
|
|
|
do { |
|
nodes.push(parseFn.call(this)); |
|
} while (this.expectOptionalToken(delimiterKind)); |
|
|
|
return nodes; |
|
} |
|
|
|
advanceLexer() { |
|
const { maxTokens } = this._options; |
|
|
|
const token = this._lexer.advance(); |
|
|
|
if (maxTokens !== undefined && token.kind !== TokenKind.EOF) { |
|
++this._tokenCounter; |
|
|
|
if (this._tokenCounter > maxTokens) { |
|
throw syntaxError( |
|
this._lexer.source, |
|
token.start, |
|
`Document contains more that ${maxTokens} tokens. Parsing aborted.`, |
|
); |
|
} |
|
} |
|
} |
|
} |
|
/** |
|
* A helper function to describe a token as a string for debugging. |
|
*/ |
|
|
|
function getTokenDesc(token) { |
|
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) { |
|
return isPunctuatorTokenKind(kind) ? `"${kind}"` : kind; |
|
}
|
|
|