Mirror: The spec-compliant minimum of client-side GraphQL.
1/** 2 * This is a spec-compliant implementation of a GraphQL query language parser, 3 * up-to-date with the October 2021 Edition. Unlike the reference implementation 4 * in graphql.js it will only parse the query language, but not the schema 5 * language. 6 */ 7import { Kind, OperationTypeNode } from './kind'; 8import { GraphQLError } from './error'; 9import { Source } from './types'; 10import type * as ast from './ast'; 11 12let input: string; 13let idx: number; 14 15function error(kind: string) { 16 return new GraphQLError(`Syntax Error: Unexpected token at ${idx} in ${kind}`); 17} 18 19function advance(pattern: RegExp) { 20 pattern.lastIndex = idx; 21 if (pattern.test(input)) { 22 const match = input.slice(idx, (idx = pattern.lastIndex)); 23 return match; 24 } 25} 26 27const leadingRe = / +(?=[^\s])/y; 28function blockString(string: string) { 29 const lines = string.split('\n'); 30 let out = ''; 31 let commonIndent = 0; 32 let firstNonEmptyLine = 0; 33 let lastNonEmptyLine = lines.length - 1; 34 for (let i = 0; i < lines.length; i++) { 35 leadingRe.lastIndex = 0; 36 if (leadingRe.test(lines[i])) { 37 if (i && (!commonIndent || leadingRe.lastIndex < commonIndent)) 38 commonIndent = leadingRe.lastIndex; 39 firstNonEmptyLine = firstNonEmptyLine || i; 40 lastNonEmptyLine = i; 41 } 42 } 43 for (let i = firstNonEmptyLine; i <= lastNonEmptyLine; i++) { 44 if (i !== firstNonEmptyLine) out += '\n'; 45 out += lines[i].slice(commonIndent).replace(/\\"""/g, '"""'); 46 } 47 return out; 48} 49 50// Note: This is equivalent to: /(?:[\s,]*|#[^\n\r]*)*/y 51function ignored() { 52 for ( 53 let char = input.charCodeAt(idx++) | 0; 54 char === 9 /*'\t'*/ || 55 char === 10 /*'\n'*/ || 56 char === 13 /*'\r'*/ || 57 char === 32 /*' '*/ || 58 char === 35 /*'#'*/ || 59 char === 44 /*','*/ || 60 char === 65279 /*'\ufeff'*/; 61 char = input.charCodeAt(idx++) | 0 62 ) { 63 if (char === 35 /*'#'*/) while ((char = input.charCodeAt(idx++)) !== 10 && char !== 13); 64 } 65 idx--; 66} 67 68const nameRe = /[_\w][_\d\w]*/y; 69function name(): ast.NameNode | undefined { 70 let match: string | undefined; 71 if ((match = advance(nameRe))) { 72 return { 73 kind: 'Name' as Kind.NAME, 74 value: match, 75 }; 76 } 77} 78 79const constRe = /null|true|false/y; 80const variableRe = /\$[_\w][_\d\w]*/y; 81const intRe = /-?\d+/y; 82const floatPartRe = /(?:\.\d+)?(?:[eE][+-]?\d+)?/y; 83const complexStringRe = /\\/g; 84const blockStringRe = /"""(?:[\s\S]+(?="""))?"""/y; 85const stringRe = /"(?:[^"\r\n]+)?"/y; 86 87function value(constant: true): ast.ConstValueNode; 88function value(constant: boolean): ast.ValueNode; 89 90function value(constant: boolean): ast.ValueNode | undefined { 91 let out: ast.ValueNode | undefined; 92 let match: string | undefined; 93 if ((match = advance(constRe))) { 94 out = 95 match === 'null' 96 ? { 97 kind: 'NullValue' as Kind.NULL, 98 } 99 : { 100 kind: 'BooleanValue' as Kind.BOOLEAN, 101 value: match === 'true', 102 }; 103 } else if (!constant && (match = advance(variableRe))) { 104 out = { 105 kind: 'Variable' as Kind.VARIABLE, 106 name: { 107 kind: 'Name' as Kind.NAME, 108 value: match.slice(1), 109 }, 110 }; 111 } else if ((match = advance(intRe))) { 112 const intPart = match; 113 if ((match = advance(floatPartRe))) { 114 out = { 115 kind: 'FloatValue' as Kind.FLOAT, 116 value: intPart + match, 117 }; 118 } else { 119 out = { 120 kind: 'IntValue' as Kind.INT, 121 value: intPart, 122 }; 123 } 124 } else if ((match = advance(nameRe))) { 125 out = { 126 kind: 'EnumValue' as Kind.ENUM, 127 value: match, 128 }; 129 } else if ((match = advance(blockStringRe))) { 130 out = { 131 kind: 'StringValue' as Kind.STRING, 132 value: blockString(match.slice(3, -3)), 133 block: true, 134 }; 135 } else if ((match = advance(stringRe))) { 136 out = { 137 kind: 'StringValue' as Kind.STRING, 138 value: complexStringRe.test(match) ? (JSON.parse(match) as string) : match.slice(1, -1), 139 block: false, 140 }; 141 } else if ((out = list(constant) || object(constant))) { 142 return out; 143 } 144 145 ignored(); 146 return out; 147} 148 149function list(constant: boolean): ast.ListValueNode | undefined { 150 let match: ast.ValueNode | undefined; 151 if (input.charCodeAt(idx) === 91 /*'['*/) { 152 idx++; 153 ignored(); 154 const values: ast.ValueNode[] = []; 155 while ((match = value(constant))) values.push(match); 156 if (input.charCodeAt(idx++) !== 93 /*']'*/) throw error('ListValue'); 157 ignored(); 158 return { 159 kind: 'ListValue' as Kind.LIST, 160 values, 161 }; 162 } 163} 164 165function object(constant: boolean): ast.ObjectValueNode | undefined { 166 if (input.charCodeAt(idx) === 123 /*'{'*/) { 167 idx++; 168 ignored(); 169 const fields: ast.ObjectFieldNode[] = []; 170 let _name: ast.NameNode | undefined; 171 while ((_name = name())) { 172 ignored(); 173 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField' as Kind.OBJECT_FIELD); 174 ignored(); 175 const _value = value(constant); 176 if (!_value) throw error('ObjectField'); 177 fields.push({ 178 kind: 'ObjectField' as Kind.OBJECT_FIELD, 179 name: _name, 180 value: _value, 181 }); 182 } 183 if (input.charCodeAt(idx++) !== 125 /*'}'*/) throw error('ObjectValue'); 184 ignored(); 185 return { 186 kind: 'ObjectValue' as Kind.OBJECT, 187 fields, 188 }; 189 } 190} 191 192function arguments_(constant: boolean): ast.ArgumentNode[] { 193 const args: ast.ArgumentNode[] = []; 194 ignored(); 195 if (input.charCodeAt(idx) === 40 /*'('*/) { 196 idx++; 197 ignored(); 198 let _name: ast.NameNode | undefined; 199 while ((_name = name())) { 200 ignored(); 201 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument'); 202 ignored(); 203 const _value = value(constant); 204 if (!_value) throw error('Argument'); 205 args.push({ 206 kind: 'Argument' as Kind.ARGUMENT, 207 name: _name, 208 value: _value, 209 }); 210 } 211 if (!args.length || input.charCodeAt(idx++) !== 41 /*')'*/) throw error('Argument'); 212 ignored(); 213 } 214 return args; 215} 216 217function directives(constant: true): ast.ConstDirectiveNode[]; 218function directives(constant: boolean): ast.DirectiveNode[]; 219 220function directives(constant: boolean): ast.DirectiveNode[] { 221 const directives: ast.DirectiveNode[] = []; 222 ignored(); 223 while (input.charCodeAt(idx) === 64 /*'@'*/) { 224 idx++; 225 const _name = name(); 226 if (!_name) throw error('Directive'); 227 ignored(); 228 directives.push({ 229 kind: 'Directive' as Kind.DIRECTIVE, 230 name: _name, 231 arguments: arguments_(constant), 232 }); 233 } 234 return directives; 235} 236 237function field(): ast.FieldNode | undefined { 238 let _name = name(); 239 if (_name) { 240 ignored(); 241 let _alias: ast.NameNode | undefined; 242 if (input.charCodeAt(idx) === 58 /*':'*/) { 243 idx++; 244 ignored(); 245 _alias = _name; 246 _name = name(); 247 if (!_name) throw error('Field'); 248 ignored(); 249 } 250 return { 251 kind: 'Field' as Kind.FIELD, 252 alias: _alias, 253 name: _name, 254 arguments: arguments_(false), 255 directives: directives(false), 256 selectionSet: selectionSet(), 257 }; 258 } 259} 260 261function type(): ast.TypeNode { 262 let match: ast.NameNode | ast.TypeNode | undefined; 263 ignored(); 264 if (input.charCodeAt(idx) === 91 /*'['*/) { 265 idx++; 266 ignored(); 267 const _type = type(); 268 if (!_type || input.charCodeAt(idx++) !== 93 /*']'*/) throw error('ListType'); 269 match = { 270 kind: 'ListType' as Kind.LIST_TYPE, 271 type: _type, 272 }; 273 } else if ((match = name())) { 274 match = { 275 kind: 'NamedType' as Kind.NAMED_TYPE, 276 name: match, 277 }; 278 } else { 279 throw error('NamedType'); 280 } 281 282 ignored(); 283 if (input.charCodeAt(idx) === 33 /*'!'*/) { 284 idx++; 285 ignored(); 286 return { 287 kind: 'NonNullType' as Kind.NON_NULL_TYPE, 288 type: match, 289 }; 290 } else { 291 return match; 292 } 293} 294 295const typeConditionRe = /on/y; 296function typeCondition(): ast.NamedTypeNode | undefined { 297 if (advance(typeConditionRe)) { 298 ignored(); 299 const _name = name(); 300 if (!_name) throw error('NamedType'); 301 ignored(); 302 return { 303 kind: 'NamedType' as Kind.NAMED_TYPE, 304 name: _name, 305 }; 306 } 307} 308 309const fragmentSpreadRe = /\.\.\./y; 310 311function fragmentSpread(): ast.FragmentSpreadNode | ast.InlineFragmentNode | undefined { 312 if (advance(fragmentSpreadRe)) { 313 ignored(); 314 const _idx = idx; 315 let _name: ast.NameNode | undefined; 316 if ((_name = name()) && _name.value !== 'on') { 317 return { 318 kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, 319 name: _name, 320 directives: directives(false), 321 }; 322 } else { 323 idx = _idx; 324 const _typeCondition = typeCondition(); 325 const _directives = directives(false); 326 const _selectionSet = selectionSet(); 327 if (!_selectionSet) throw error('InlineFragment'); 328 return { 329 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, 330 typeCondition: _typeCondition, 331 directives: _directives, 332 selectionSet: _selectionSet, 333 }; 334 } 335 } 336} 337 338function selectionSet(): ast.SelectionSetNode | undefined { 339 let match: ast.SelectionNode | undefined; 340 ignored(); 341 if (input.charCodeAt(idx) === 123 /*'{'*/) { 342 idx++; 343 ignored(); 344 const selections: ast.SelectionNode[] = []; 345 while ((match = fragmentSpread() || field())) selections.push(match); 346 if (!selections.length || input.charCodeAt(idx++) !== 125 /*'}'*/) throw error('SelectionSet'); 347 ignored(); 348 return { 349 kind: 'SelectionSet' as Kind.SELECTION_SET, 350 selections, 351 }; 352 } 353} 354 355function variableDefinitions(): ast.VariableDefinitionNode[] { 356 let match: string | undefined; 357 const vars: ast.VariableDefinitionNode[] = []; 358 ignored(); 359 if (input.charCodeAt(idx) === 40 /*'('*/) { 360 idx++; 361 ignored(); 362 while ((match = advance(variableRe))) { 363 ignored(); 364 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition'); 365 const _type = type(); 366 let _defaultValue: ast.ValueNode | undefined; 367 if (input.charCodeAt(idx) === 61 /*'='*/) { 368 idx++; 369 ignored(); 370 _defaultValue = value(true); 371 if (!_defaultValue) throw error('VariableDefinition'); 372 } 373 ignored(); 374 vars.push({ 375 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION, 376 variable: { 377 kind: 'Variable' as Kind.VARIABLE, 378 name: { 379 kind: 'Name' as Kind.NAME, 380 value: match.slice(1), 381 }, 382 }, 383 type: _type, 384 defaultValue: _defaultValue as ast.ConstValueNode, 385 directives: directives(true), 386 }); 387 } 388 if (input.charCodeAt(idx++) !== 41 /*')'*/) throw error('VariableDefinition'); 389 ignored(); 390 } 391 return vars; 392} 393 394const fragmentDefinitionRe = /fragment/y; 395function fragmentDefinition(): ast.FragmentDefinitionNode | undefined { 396 if (advance(fragmentDefinitionRe)) { 397 ignored(); 398 const _name = name(); 399 if (!_name) throw error('FragmentDefinition'); 400 ignored(); 401 const _typeCondition = typeCondition(); 402 if (!_typeCondition) throw error('FragmentDefinition'); 403 const _directives = directives(false); 404 const _selectionSet = selectionSet(); 405 if (!_selectionSet) throw error('FragmentDefinition'); 406 return { 407 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, 408 name: _name, 409 typeCondition: _typeCondition, 410 directives: _directives, 411 selectionSet: _selectionSet, 412 }; 413 } 414} 415 416const operationDefinitionRe = /query|mutation|subscription/y; 417function operationDefinition(): ast.OperationDefinitionNode | undefined { 418 let _operation: string | undefined; 419 let _name: ast.NameNode | undefined; 420 let _variableDefinitions: ast.VariableDefinitionNode[] = []; 421 let _directives: ast.DirectiveNode[] = []; 422 if ((_operation = advance(operationDefinitionRe))) { 423 ignored(); 424 _name = name(); 425 _variableDefinitions = variableDefinitions(); 426 _directives = directives(false); 427 } 428 const _selectionSet = selectionSet(); 429 if (_selectionSet) { 430 return { 431 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 432 operation: (_operation || 'query') as OperationTypeNode, 433 name: _name, 434 variableDefinitions: _variableDefinitions, 435 directives: _directives, 436 selectionSet: _selectionSet, 437 }; 438 } 439} 440 441function document(): ast.DocumentNode { 442 let match: ast.ExecutableDefinitionNode | void; 443 ignored(); 444 const definitions: ast.ExecutableDefinitionNode[] = []; 445 while ((match = fragmentDefinition() || operationDefinition())) definitions.push(match); 446 return { 447 kind: 'Document' as Kind.DOCUMENT, 448 definitions, 449 }; 450} 451 452type ParseOptions = { 453 [option: string]: any; 454}; 455 456export function parse( 457 string: string | Source, 458 _options?: ParseOptions | undefined 459): ast.DocumentNode { 460 input = typeof string.body === 'string' ? string.body : string; 461 idx = 0; 462 return document(); 463} 464 465export function parseValue( 466 string: string | Source, 467 _options?: ParseOptions | undefined 468): ast.ValueNode { 469 input = typeof string.body === 'string' ? string.body : string; 470 idx = 0; 471 ignored(); 472 const _value = value(false); 473 if (!_value) throw error('ValueNode'); 474 return _value; 475} 476 477export function parseType( 478 string: string | Source, 479 _options?: ParseOptions | undefined 480): ast.TypeNode { 481 input = typeof string.body === 'string' ? string.body : string; 482 idx = 0; 483 return type(); 484}