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 if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable'); 425 const name = nameNode(); 426 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition'); 427 ignored(); 428 const _type = type(); 429 let _defaultValue: ast.ConstValueNode | undefined; 430 if (input.charCodeAt(idx) === 61 /*'='*/) { 431 idx++; 432 ignored(); 433 _defaultValue = value(true); 434 } 435 ignored(); 436 vars.push({ 437 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION, 438 variable: { 439 kind: 'Variable' as Kind.VARIABLE, 440 name, 441 }, 442 type: _type, 443 defaultValue: _defaultValue, 444 directives: directives(true), 445 }); 446 } while (input.charCodeAt(idx) !== 41 /*')'*/); 447 idx++; 448 ignored(); 449 return vars; 450 } 451} 452 453function fragmentDefinition(): ast.FragmentDefinitionNode { 454 const name = nameNode(); 455 if (input.charCodeAt(idx++) !== 111 /*'o'*/ || input.charCodeAt(idx++) !== 110 /*'n'*/) 456 throw error('FragmentDefinition'); 457 ignored(); 458 return { 459 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, 460 name, 461 typeCondition: { 462 kind: 'NamedType' as Kind.NAMED_TYPE, 463 name: nameNode(), 464 }, 465 directives: directives(false), 466 selectionSet: selectionSetStart(), 467 }; 468} 469 470function definitions(): ast.DefinitionNode[] { 471 const _definitions: ast.ExecutableDefinitionNode[] = []; 472 do { 473 if (input.charCodeAt(idx) === 123 /*'{'*/) { 474 idx++; 475 ignored(); 476 _definitions.push({ 477 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 478 operation: 'query' as OperationTypeNode.QUERY, 479 name: undefined, 480 variableDefinitions: undefined, 481 directives: undefined, 482 selectionSet: selectionSet(), 483 }); 484 } else { 485 const definition = name(); 486 switch (definition) { 487 case 'fragment': 488 _definitions.push(fragmentDefinition()); 489 break; 490 case 'query': 491 case 'mutation': 492 case 'subscription': 493 let char: number; 494 let name: ast.NameNode | undefined; 495 if ( 496 (char = input.charCodeAt(idx)) !== 40 /*'('*/ && 497 char !== 64 /*'@'*/ && 498 char !== 123 /*'{'*/ 499 ) { 500 name = nameNode(); 501 } 502 _definitions.push({ 503 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 504 operation: definition as OperationTypeNode, 505 name, 506 variableDefinitions: variableDefinitions(), 507 directives: directives(false), 508 selectionSet: selectionSetStart(), 509 }); 510 break; 511 default: 512 throw error('Document'); 513 } 514 } 515 } while (idx < input.length); 516 return _definitions; 517} 518 519type ParseOptions = { 520 [option: string]: any; 521}; 522 523export function parse( 524 string: string | Source, 525 options?: ParseOptions | undefined 526): ast.DocumentNode { 527 input = string.body ? string.body : string; 528 idx = 0; 529 ignored(); 530 if (options && options.noLocation) { 531 return { 532 kind: 'Document' as Kind.DOCUMENT, 533 definitions: definitions(), 534 }; 535 } else { 536 return { 537 kind: 'Document' as Kind.DOCUMENT, 538 definitions: definitions(), 539 loc: { 540 start: 0, 541 end: input.length, 542 startToken: undefined, 543 endToken: undefined, 544 source: { 545 body: input, 546 name: 'graphql.web', 547 locationOffset: { line: 1, column: 1 }, 548 }, 549 }, 550 } as Location; 551 } 552} 553 554export function parseValue( 555 string: string | Source, 556 _options?: ParseOptions | undefined 557): ast.ValueNode { 558 input = string.body ? string.body : string; 559 idx = 0; 560 ignored(); 561 return value(false); 562} 563 564export function parseType( 565 string: string | Source, 566 _options?: ParseOptions | undefined 567): ast.TypeNode { 568 input = string.body ? string.body : string; 569 idx = 0; 570 return type(); 571}