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