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 type { Kind, OperationTypeNode } from './kind'; 8import { GraphQLError } from './error'; 9import type { Location, 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 68function name(): string { 69 const start = idx; 70 for ( 71 let char = input.charCodeAt(idx++) | 0; 72 (char >= 48 /*'0'*/ && char <= 57) /*'9'*/ || 73 (char >= 65 /*'A'*/ && char <= 90) /*'Z'*/ || 74 char === 95 /*'_'*/ || 75 (char >= 97 /*'a'*/ && char <= 122) /*'z'*/; 76 char = input.charCodeAt(idx++) | 0 77 ); 78 if (start === idx - 1) throw error('Name'); 79 const value = input.slice(start, --idx); 80 ignored(); 81 return value; 82} 83 84function nameNode(): ast.NameNode { 85 return { 86 kind: 'Name' as Kind.NAME, 87 value: name(), 88 }; 89} 90 91const restBlockStringRe = /(?:"""|(?:[\s\S]*?[^\\])""")/y; 92const floatPartRe = /(?:(?:\.\d+)?[eE][+-]?\d+|\.\d+)/y; 93 94function value(constant: true): ast.ConstValueNode; 95function value(constant: boolean): ast.ValueNode; 96 97function value(constant: boolean): ast.ValueNode { 98 let match: string | undefined; 99 switch (input.charCodeAt(idx)) { 100 case 91: // '[' 101 idx++; 102 ignored(); 103 const values: ast.ValueNode[] = []; 104 while (input.charCodeAt(idx) !== 93 /*']'*/) values.push(value(constant)); 105 idx++; 106 ignored(); 107 return { 108 kind: 'ListValue' as Kind.LIST, 109 values, 110 }; 111 112 case 123: // '{' 113 idx++; 114 ignored(); 115 const fields: ast.ObjectFieldNode[] = []; 116 while (input.charCodeAt(idx) !== 125 /*'}'*/) { 117 const name = nameNode(); 118 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField'); 119 ignored(); 120 fields.push({ 121 kind: 'ObjectField' as Kind.OBJECT_FIELD, 122 name, 123 value: value(constant), 124 }); 125 } 126 idx++; 127 ignored(); 128 return { 129 kind: 'ObjectValue' as Kind.OBJECT, 130 fields, 131 }; 132 133 case 36: // '$' 134 if (constant) throw error('Variable'); 135 idx++; 136 return { 137 kind: 'Variable' as Kind.VARIABLE, 138 name: nameNode(), 139 }; 140 141 case 34: // '"' 142 if (input.charCodeAt(idx + 1) === 34 && input.charCodeAt(idx + 2) === 34) { 143 idx += 3; 144 if ((match = advance(restBlockStringRe)) == null) throw error('StringValue'); 145 ignored(); 146 return { 147 kind: 'StringValue' as Kind.STRING, 148 value: blockString(match.slice(0, -3)), 149 block: true, 150 }; 151 } else { 152 const start = idx; 153 idx++; 154 let char: number; 155 let isComplex = false; 156 for ( 157 char = input.charCodeAt(idx++) | 0; 158 (char === 92 /*'\\'*/ && (idx++, (isComplex = true))) || 159 (char !== 10 /*'\n'*/ && char !== 13 /*'\r'*/ && char !== 34 /*'"'*/ && char); 160 char = input.charCodeAt(idx++) | 0 161 ); 162 if (char !== 34) throw error('StringValue'); 163 match = input.slice(start, idx); 164 ignored(); 165 return { 166 kind: 'StringValue' as Kind.STRING, 167 value: isComplex ? (JSON.parse(match) as string) : match.slice(1, -1), 168 block: false, 169 }; 170 } 171 172 case 45: // '-' 173 case 48: // '0' 174 case 49: // '1' 175 case 50: // '2' 176 case 51: // '3' 177 case 52: // '4' 178 case 53: // '5' 179 case 54: // '6' 180 case 55: // '7' 181 case 56: // '8' 182 case 57: // '9' 183 const start = idx++; 184 let char: number; 185 while ((char = input.charCodeAt(idx++) | 0) >= 48 /*'0'*/ && char <= 57 /*'9'*/); 186 const intPart = input.slice(start, --idx); 187 if ( 188 (char = input.charCodeAt(idx)) === 46 /*'.'*/ || 189 char === 69 /*'E'*/ || 190 char === 101 /*'e'*/ 191 ) { 192 if ((match = advance(floatPartRe)) == null) throw error('FloatValue'); 193 ignored(); 194 return { 195 kind: 'FloatValue' as Kind.FLOAT, 196 value: intPart + match, 197 }; 198 } else { 199 ignored(); 200 return { 201 kind: 'IntValue' as Kind.INT, 202 value: intPart, 203 }; 204 } 205 206 case 110: // 'n' 207 if ( 208 input.charCodeAt(idx + 1) === 117 && 209 input.charCodeAt(idx + 2) === 108 && 210 input.charCodeAt(idx + 3) === 108 211 ) { 212 idx += 4; 213 ignored(); 214 return { kind: 'NullValue' as Kind.NULL }; 215 } else break; 216 217 case 116: // 't' 218 if ( 219 input.charCodeAt(idx + 1) === 114 && 220 input.charCodeAt(idx + 2) === 117 && 221 input.charCodeAt(idx + 3) === 101 222 ) { 223 idx += 4; 224 ignored(); 225 return { kind: 'BooleanValue' as Kind.BOOLEAN, value: true }; 226 } else break; 227 228 case 102: // 'f' 229 if ( 230 input.charCodeAt(idx + 1) === 97 && 231 input.charCodeAt(idx + 2) === 108 && 232 input.charCodeAt(idx + 3) === 115 && 233 input.charCodeAt(idx + 4) === 101 234 ) { 235 idx += 5; 236 ignored(); 237 return { kind: 'BooleanValue' as Kind.BOOLEAN, value: false }; 238 } else break; 239 } 240 241 return { 242 kind: 'EnumValue' as Kind.ENUM, 243 value: name(), 244 }; 245} 246 247function arguments_(constant: boolean): ast.ArgumentNode[] | undefined { 248 if (input.charCodeAt(idx) === 40 /*'('*/) { 249 const args: ast.ArgumentNode[] = []; 250 idx++; 251 ignored(); 252 do { 253 const name = nameNode(); 254 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument'); 255 ignored(); 256 args.push({ 257 kind: 'Argument' as Kind.ARGUMENT, 258 name, 259 value: value(constant), 260 }); 261 } while (input.charCodeAt(idx) !== 41 /*')'*/); 262 idx++; 263 ignored(); 264 return args; 265 } 266} 267 268function directives(constant: true): ast.ConstDirectiveNode[] | undefined; 269function directives(constant: boolean): ast.DirectiveNode[] | undefined; 270 271function directives(constant: boolean): ast.DirectiveNode[] | undefined { 272 if (input.charCodeAt(idx) === 64 /*'@'*/) { 273 const directives: ast.DirectiveNode[] = []; 274 do { 275 idx++; 276 directives.push({ 277 kind: 'Directive' as Kind.DIRECTIVE, 278 name: nameNode(), 279 arguments: arguments_(constant), 280 }); 281 } while (input.charCodeAt(idx) === 64 /*'@'*/); 282 return directives; 283 } 284} 285 286function type(): ast.TypeNode { 287 let lists = 0; 288 while (input.charCodeAt(idx) === 91 /*'['*/) { 289 lists++; 290 idx++; 291 ignored(); 292 } 293 let type: ast.TypeNode = { 294 kind: 'NamedType' as Kind.NAMED_TYPE, 295 name: nameNode(), 296 }; 297 do { 298 if (input.charCodeAt(idx) === 33 /*'!'*/) { 299 idx++; 300 ignored(); 301 type = { 302 kind: 'NonNullType' as Kind.NON_NULL_TYPE, 303 type: type as ast.NamedTypeNode | ast.ListTypeNode, 304 } satisfies ast.NonNullTypeNode; 305 } 306 if (lists) { 307 if (input.charCodeAt(idx++) !== 93 /*']'*/) throw error('NamedType'); 308 ignored(); 309 type = { 310 kind: 'ListType' as Kind.LIST_TYPE, 311 type: type as ast.NamedTypeNode | ast.ListTypeNode, 312 } satisfies ast.ListTypeNode; 313 } 314 } while (lists--); 315 return type; 316} 317 318function selectionSetStart(): ast.SelectionSetNode { 319 if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('SelectionSet'); 320 ignored(); 321 return selectionSet(); 322} 323 324function selectionSet(): ast.SelectionSetNode { 325 const selections: ast.SelectionNode[] = []; 326 do { 327 if (input.charCodeAt(idx) === 46 /*'.'*/) { 328 if (input.charCodeAt(++idx) !== 46 /*'.'*/ || input.charCodeAt(++idx) !== 46 /*'.'*/) 329 throw error('SelectionSet'); 330 idx++; 331 ignored(); 332 switch (input.charCodeAt(idx)) { 333 case 64 /*'@'*/: 334 selections.push({ 335 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, 336 typeCondition: undefined, 337 directives: directives(false), 338 selectionSet: selectionSetStart(), 339 }); 340 break; 341 342 case 111 /*'o'*/: 343 if (input.charCodeAt(idx + 1) === 110 /*'n'*/) { 344 idx += 2; 345 ignored(); 346 selections.push({ 347 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, 348 typeCondition: { 349 kind: 'NamedType' as Kind.NAMED_TYPE, 350 name: nameNode(), 351 }, 352 directives: directives(false), 353 selectionSet: selectionSetStart(), 354 }); 355 } else { 356 selections.push({ 357 kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, 358 name: nameNode(), 359 directives: directives(false), 360 }); 361 } 362 break; 363 364 case 123 /*'{'*/: 365 idx++; 366 ignored(); 367 selections.push({ 368 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, 369 typeCondition: undefined, 370 directives: undefined, 371 selectionSet: selectionSet(), 372 }); 373 break; 374 375 default: 376 selections.push({ 377 kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, 378 name: nameNode(), 379 directives: directives(false), 380 }); 381 } 382 } else { 383 let name = nameNode(); 384 let alias: ast.NameNode | undefined; 385 if (input.charCodeAt(idx) === 58 /*':'*/) { 386 idx++; 387 ignored(); 388 alias = name; 389 name = nameNode(); 390 } 391 const _arguments = arguments_(false); 392 const _directives = directives(false); 393 let _selectionSet: ast.SelectionSetNode | undefined; 394 if (input.charCodeAt(idx) === 123 /*'{'*/) { 395 idx++; 396 ignored(); 397 _selectionSet = selectionSet(); 398 } 399 selections.push({ 400 kind: 'Field' as Kind.FIELD, 401 alias, 402 name, 403 arguments: _arguments, 404 directives: _directives, 405 selectionSet: _selectionSet, 406 }); 407 } 408 } while (input.charCodeAt(idx) !== 125 /*'}'*/); 409 idx++; 410 ignored(); 411 return { 412 kind: 'SelectionSet' as Kind.SELECTION_SET, 413 selections, 414 }; 415} 416 417function variableDefinitions(): ast.VariableDefinitionNode[] | undefined { 418 ignored(); 419 if (input.charCodeAt(idx) === 40 /*'('*/) { 420 const vars: ast.VariableDefinitionNode[] = []; 421 idx++; 422 ignored(); 423 do { 424 let _description: ast.StringValueNode | undefined; 425 if (input.charCodeAt(idx) === 34 /*'"'*/) { 426 _description = value(true) as ast.StringValueNode; 427 } 428 if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable'); 429 const name = nameNode(); 430 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition'); 431 ignored(); 432 const _type = type(); 433 let _defaultValue: ast.ConstValueNode | undefined; 434 if (input.charCodeAt(idx) === 61 /*'='*/) { 435 idx++; 436 ignored(); 437 _defaultValue = value(true); 438 } 439 ignored(); 440 const varDef: ast.VariableDefinitionNode = { 441 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION, 442 variable: { 443 kind: 'Variable' as Kind.VARIABLE, 444 name, 445 }, 446 type: _type, 447 defaultValue: _defaultValue, 448 directives: directives(true), 449 }; 450 if (_description) { 451 varDef.description = _description; 452 } 453 vars.push(varDef); 454 } while (input.charCodeAt(idx) !== 41 /*')'*/); 455 idx++; 456 ignored(); 457 return vars; 458 } 459} 460 461function fragmentDefinition(description?: ast.StringValueNode): ast.FragmentDefinitionNode { 462 const name = nameNode(); 463 if (input.charCodeAt(idx++) !== 111 /*'o'*/ || input.charCodeAt(idx++) !== 110 /*'n'*/) 464 throw error('FragmentDefinition'); 465 ignored(); 466 const fragDef: ast.FragmentDefinitionNode = { 467 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, 468 name, 469 typeCondition: { 470 kind: 'NamedType' as Kind.NAMED_TYPE, 471 name: nameNode(), 472 }, 473 directives: directives(false), 474 selectionSet: selectionSetStart(), 475 }; 476 if (description) { 477 fragDef.description = description; 478 } 479 return fragDef; 480} 481 482function definitions(): ast.DefinitionNode[] { 483 const _definitions: ast.ExecutableDefinitionNode[] = []; 484 do { 485 let _description: ast.StringValueNode | undefined; 486 if (input.charCodeAt(idx) === 34 /*'"'*/) { 487 _description = value(true) as ast.StringValueNode; 488 } 489 if (input.charCodeAt(idx) === 123 /*'{'*/) { 490 // Anonymous operations can't have descriptions 491 if (_description) throw error('Document'); 492 idx++; 493 ignored(); 494 _definitions.push({ 495 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 496 operation: 'query' as OperationTypeNode.QUERY, 497 name: undefined, 498 variableDefinitions: undefined, 499 directives: undefined, 500 selectionSet: selectionSet(), 501 }); 502 } else { 503 const definition = name(); 504 switch (definition) { 505 case 'fragment': 506 _definitions.push(fragmentDefinition(_description)); 507 break; 508 case 'query': 509 case 'mutation': 510 case 'subscription': 511 let char: number; 512 let name: ast.NameNode | undefined; 513 if ( 514 (char = input.charCodeAt(idx)) !== 40 /*'('*/ && 515 char !== 64 /*'@'*/ && 516 char !== 123 /*'{'*/ 517 ) { 518 name = nameNode(); 519 } 520 const opDef: ast.OperationDefinitionNode = { 521 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 522 operation: definition as OperationTypeNode, 523 name, 524 variableDefinitions: variableDefinitions(), 525 directives: directives(false), 526 selectionSet: selectionSetStart(), 527 }; 528 if (_description) { 529 opDef.description = _description; 530 } 531 _definitions.push(opDef); 532 break; 533 default: 534 throw error('Document'); 535 } 536 } 537 } while (idx < input.length); 538 return _definitions; 539} 540 541type ParseOptions = { 542 [option: string]: any; 543}; 544 545export function parse( 546 string: string | Source, 547 options?: ParseOptions | undefined 548): ast.DocumentNode { 549 input = string.body ? string.body : string; 550 idx = 0; 551 ignored(); 552 if (options && options.noLocation) { 553 return { 554 kind: 'Document' as Kind.DOCUMENT, 555 definitions: definitions(), 556 }; 557 } else { 558 return { 559 kind: 'Document' as Kind.DOCUMENT, 560 definitions: definitions(), 561 loc: { 562 start: 0, 563 end: input.length, 564 startToken: undefined, 565 endToken: undefined, 566 source: { 567 body: input, 568 name: 'graphql.web', 569 locationOffset: { line: 1, column: 1 }, 570 }, 571 }, 572 } as Location; 573 } 574} 575 576export function parseValue( 577 string: string | Source, 578 _options?: ParseOptions | undefined 579): ast.ValueNode { 580 input = string.body ? string.body : string; 581 idx = 0; 582 ignored(); 583 return value(false); 584} 585 586export function parseType( 587 string: string | Source, 588 _options?: ParseOptions | undefined 589): ast.TypeNode { 590 input = string.body ? string.body : string; 591 idx = 0; 592 return type(); 593}