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 68const nameRe = /[_A-Za-z]\w*/y; 69 70// NOTE: This should be compressed by our build step 71// This merges all possible value parsing into one regular expression 72const valueRe = new RegExp( 73 '(?:' + 74 // `null`, `true`, and `false` literals (BooleanValue & NullValue) 75 '(null|true|false)|' + 76 // Variables starting with `$` then having a name (VariableNode) 77 '\\$(' + 78 nameRe.source + 79 ')|' + 80 // Numbers, starting with int then optionally following with a float part (IntValue and FloatValue) 81 '(-?\\d+)((?:\\.\\d+)?[eE][+-]?\\d+|\\.\\d+)?|' + 82 // Block strings starting with `"""` until the next unescaped `"""` (StringValue) 83 '("""(?:"""|(?:[\\s\\S]*?[^\\\\])"""))|' + 84 // Strings starting with `"` must be on one line (StringValue) 85 '("(?:"|[^\\r\\n]*?[^\\\\]"))|' + // string 86 // Enums are simply names except for our literals (EnumValue) 87 '(' + 88 nameRe.source + 89 '))', 90 'y' 91); 92 93// NOTE: Each of the groups above end up in the RegExpExecArray at the specified indices (starting with 1) 94const enum ValueGroup { 95 Const = 1, 96 Var, 97 Int, 98 Float, 99 BlockString, 100 String, 101 Enum, 102} 103 104type ValueExec = RegExpExecArray & { 105 [Prop in ValueGroup]: string | undefined; 106}; 107 108const complexStringRe = /\\/; 109 110function value(constant: true): ast.ConstValueNode; 111function value(constant: boolean): ast.ValueNode; 112 113function value(constant: boolean): ast.ValueNode { 114 let match: string | undefined; 115 let exec: ValueExec | null; 116 valueRe.lastIndex = idx; 117 if (input.charCodeAt(idx) === 91 /*'['*/) { 118 // Lists are checked ahead of time with `[` chars 119 idx++; 120 ignored(); 121 const values: ast.ValueNode[] = []; 122 while (input.charCodeAt(idx) !== 93 /*']'*/) values.push(value(constant)); 123 idx++; 124 ignored(); 125 return { 126 kind: 'ListValue' as Kind.LIST, 127 values, 128 }; 129 } else if (input.charCodeAt(idx) === 123 /*'{'*/) { 130 // Objects are checked ahead of time with `{` chars 131 idx++; 132 ignored(); 133 const fields: ast.ObjectFieldNode[] = []; 134 while (input.charCodeAt(idx) !== 125 /*'}'*/) { 135 if ((match = advance(nameRe)) == null) throw error('ObjectField'); 136 ignored(); 137 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField'); 138 ignored(); 139 fields.push({ 140 kind: 'ObjectField' as Kind.OBJECT_FIELD, 141 name: { kind: 'Name' as Kind.NAME, value: match }, 142 value: value(constant), 143 }); 144 } 145 idx++; 146 ignored(); 147 return { 148 kind: 'ObjectValue' as Kind.OBJECT, 149 fields, 150 }; 151 } else if ((exec = valueRe.exec(input) as ValueExec) != null) { 152 // Starting from here, the merged `valueRe` is used 153 idx = valueRe.lastIndex; 154 ignored(); 155 if ((match = exec[ValueGroup.Const]) != null) { 156 return match === 'null' 157 ? { kind: 'NullValue' as Kind.NULL } 158 : { 159 kind: 'BooleanValue' as Kind.BOOLEAN, 160 value: match === 'true', 161 }; 162 } else if ((match = exec[ValueGroup.Var]) != null) { 163 if (constant) { 164 throw error('Variable'); 165 } else { 166 return { 167 kind: 'Variable' as Kind.VARIABLE, 168 name: { 169 kind: 'Name' as Kind.NAME, 170 value: match, 171 }, 172 }; 173 } 174 } else if ((match = exec[ValueGroup.Int]) != null) { 175 let floatPart: string | undefined; 176 if ((floatPart = exec[ValueGroup.Float]) != null) { 177 return { 178 kind: 'FloatValue' as Kind.FLOAT, 179 value: match + floatPart, 180 }; 181 } else { 182 return { 183 kind: 'IntValue' as Kind.INT, 184 value: match, 185 }; 186 } 187 } else if ((match = exec[ValueGroup.BlockString]) != null) { 188 return { 189 kind: 'StringValue' as Kind.STRING, 190 value: blockString(match.slice(3, -3)), 191 block: true, 192 }; 193 } else if ((match = exec[ValueGroup.String]) != null) { 194 return { 195 kind: 'StringValue' as Kind.STRING, 196 // When strings don't contain escape codes, a simple slice will be enough, otherwise 197 // `JSON.parse` matches GraphQL's string parsing perfectly 198 value: complexStringRe.test(match) ? (JSON.parse(match) as string) : match.slice(1, -1), 199 block: false, 200 }; 201 } else if ((match = exec[ValueGroup.Enum]) != null) { 202 return { 203 kind: 'EnumValue' as Kind.ENUM, 204 value: match, 205 }; 206 } 207 } 208 209 throw error('Value'); 210} 211 212function arguments_(constant: boolean): ast.ArgumentNode[] | undefined { 213 if (input.charCodeAt(idx) === 40 /*'('*/) { 214 const args: ast.ArgumentNode[] = []; 215 idx++; 216 ignored(); 217 let _name: string | undefined; 218 do { 219 if ((_name = advance(nameRe)) == null) throw error('Argument'); 220 ignored(); 221 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument'); 222 ignored(); 223 args.push({ 224 kind: 'Argument' as Kind.ARGUMENT, 225 name: { kind: 'Name' as Kind.NAME, value: _name }, 226 value: value(constant), 227 }); 228 } while (input.charCodeAt(idx) !== 41 /*')'*/); 229 idx++; 230 ignored(); 231 return args; 232 } 233} 234 235function directives(constant: true): ast.ConstDirectiveNode[] | undefined; 236function directives(constant: boolean): ast.DirectiveNode[] | undefined; 237 238function directives(constant: boolean): ast.DirectiveNode[] | undefined { 239 if (input.charCodeAt(idx) === 64 /*'@'*/) { 240 const directives: ast.DirectiveNode[] = []; 241 let _name: string | undefined; 242 do { 243 idx++; 244 if ((_name = advance(nameRe)) == null) throw error('Directive'); 245 ignored(); 246 directives.push({ 247 kind: 'Directive' as Kind.DIRECTIVE, 248 name: { kind: 'Name' as Kind.NAME, value: _name }, 249 arguments: arguments_(constant), 250 }); 251 } while (input.charCodeAt(idx) === 64 /*'@'*/); 252 return directives; 253 } 254} 255 256function type(): ast.TypeNode { 257 let match: string | undefined; 258 let lists = 0; 259 while (input.charCodeAt(idx) === 91 /*'['*/) { 260 lists++; 261 idx++; 262 ignored(); 263 } 264 if ((match = advance(nameRe)) == null) throw error('NamedType'); 265 ignored(); 266 let type: ast.TypeNode = { 267 kind: 'NamedType' as Kind.NAMED_TYPE, 268 name: { kind: 'Name' as Kind.NAME, value: match }, 269 }; 270 do { 271 if (input.charCodeAt(idx) === 33 /*'!'*/) { 272 idx++; 273 ignored(); 274 type = { 275 kind: 'NonNullType' as Kind.NON_NULL_TYPE, 276 type: type as ast.NamedTypeNode | ast.ListTypeNode, 277 } satisfies ast.NonNullTypeNode; 278 } 279 if (lists) { 280 if (input.charCodeAt(idx++) !== 93 /*']'*/) throw error('NamedType'); 281 ignored(); 282 type = { 283 kind: 'ListType' as Kind.LIST_TYPE, 284 type: type as ast.NamedTypeNode | ast.ListTypeNode, 285 } satisfies ast.ListTypeNode; 286 } 287 } while (lists--); 288 return type; 289} 290 291// NOTE: This should be compressed by our build step 292// This merges the two possible selection parsing branches into one regular expression 293const selectionRe = new RegExp( 294 '(?:' + 295 // fragment spreads (FragmentSpread or InlineFragment nodes) 296 '(\\.{3})|' + 297 // field aliases or names (FieldNode) 298 '(' + 299 nameRe.source + 300 '))', 301 'y' 302); 303 304// NOTE: Each of the groups above end up in the RegExpExecArray at the indices 1&2 305const enum SelectionGroup { 306 Spread = 1, 307 Name, 308} 309 310type SelectionExec = RegExpExecArray & { 311 [Prop in SelectionGroup]: string | undefined; 312}; 313 314function selectionSet(): ast.SelectionSetNode { 315 const selections: ast.SelectionNode[] = []; 316 let match: string | undefined; 317 let exec: SelectionExec | null; 318 do { 319 selectionRe.lastIndex = idx; 320 if ((exec = selectionRe.exec(input) as SelectionExec) != null) { 321 idx = selectionRe.lastIndex; 322 if (exec[SelectionGroup.Spread] != null) { 323 ignored(); 324 let match = advance(nameRe); 325 if (match != null && match !== 'on') { 326 // A simple `...Name` spread with optional directives 327 ignored(); 328 selections.push({ 329 kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, 330 name: { kind: 'Name' as Kind.NAME, value: match }, 331 directives: directives(false), 332 }); 333 } else { 334 ignored(); 335 if (match === 'on') { 336 // An inline `... on Name` spread; if this doesn't match, the type condition has been omitted 337 if ((match = advance(nameRe)) == null) throw error('NamedType'); 338 ignored(); 339 } 340 const _directives = directives(false); 341 if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('InlineFragment'); 342 ignored(); 343 selections.push({ 344 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, 345 typeCondition: match 346 ? { 347 kind: 'NamedType' as Kind.NAMED_TYPE, 348 name: { kind: 'Name' as Kind.NAME, value: match }, 349 } 350 : undefined, 351 directives: _directives, 352 selectionSet: selectionSet(), 353 }); 354 } 355 } else if ((match = exec[SelectionGroup.Name]) != null) { 356 let _alias: string | undefined; 357 ignored(); 358 // Parse the optional alias, by reassigning and then getting the name 359 if (input.charCodeAt(idx) === 58 /*':'*/) { 360 idx++; 361 ignored(); 362 _alias = match; 363 if ((match = advance(nameRe)) == null) throw error('Field'); 364 ignored(); 365 } 366 const _arguments = arguments_(false); 367 ignored(); 368 const _directives = directives(false); 369 let _selectionSet: ast.SelectionSetNode | undefined; 370 if (input.charCodeAt(idx) === 123 /*'{'*/) { 371 idx++; 372 ignored(); 373 _selectionSet = selectionSet(); 374 } 375 selections.push({ 376 kind: 'Field' as Kind.FIELD, 377 alias: _alias ? { kind: 'Name' as Kind.NAME, value: _alias } : undefined, 378 name: { kind: 'Name' as Kind.NAME, value: match }, 379 arguments: _arguments, 380 directives: _directives, 381 selectionSet: _selectionSet, 382 }); 383 } 384 } else { 385 throw error('SelectionSet'); 386 } 387 } while (input.charCodeAt(idx) !== 125 /*'}'*/); 388 idx++; 389 ignored(); 390 return { 391 kind: 'SelectionSet' as Kind.SELECTION_SET, 392 selections, 393 }; 394} 395 396function variableDefinitions(): ast.VariableDefinitionNode[] | undefined { 397 ignored(); 398 if (input.charCodeAt(idx) === 40 /*'('*/) { 399 const vars: ast.VariableDefinitionNode[] = []; 400 idx++; 401 ignored(); 402 let _name: string | undefined; 403 do { 404 if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable'); 405 if ((_name = advance(nameRe)) == null) throw error('Variable'); 406 ignored(); 407 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition'); 408 ignored(); 409 const _type = type(); 410 let _defaultValue: ast.ConstValueNode | undefined; 411 if (input.charCodeAt(idx) === 61 /*'='*/) { 412 idx++; 413 ignored(); 414 _defaultValue = value(true); 415 } 416 ignored(); 417 vars.push({ 418 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION, 419 variable: { 420 kind: 'Variable' as Kind.VARIABLE, 421 name: { kind: 'Name' as Kind.NAME, value: _name }, 422 }, 423 type: _type, 424 defaultValue: _defaultValue, 425 directives: directives(true), 426 }); 427 } while (input.charCodeAt(idx) !== 41 /*')'*/); 428 idx++; 429 ignored(); 430 return vars; 431 } 432} 433 434function fragmentDefinition(): ast.FragmentDefinitionNode { 435 let _name: string | undefined; 436 let _condition: string | undefined; 437 if ((_name = advance(nameRe)) == null) throw error('FragmentDefinition'); 438 ignored(); 439 if (advance(nameRe) !== 'on') throw error('FragmentDefinition'); 440 ignored(); 441 if ((_condition = advance(nameRe)) == null) throw error('FragmentDefinition'); 442 ignored(); 443 const _directives = directives(false); 444 if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('FragmentDefinition'); 445 ignored(); 446 return { 447 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, 448 name: { kind: 'Name' as Kind.NAME, value: _name }, 449 typeCondition: { 450 kind: 'NamedType' as Kind.NAMED_TYPE, 451 name: { kind: 'Name' as Kind.NAME, value: _condition }, 452 }, 453 directives: _directives, 454 selectionSet: selectionSet(), 455 }; 456} 457 458const definitionRe = /(?:query|mutation|subscription|fragment)/y; 459 460function operationDefinition( 461 operation: OperationTypeNode | undefined 462): ast.OperationDefinitionNode | undefined { 463 let _name: string | undefined; 464 let _variableDefinitions: ast.VariableDefinitionNode[] | undefined; 465 let _directives: ast.DirectiveNode[] | undefined; 466 if (operation) { 467 ignored(); 468 _name = advance(nameRe); 469 _variableDefinitions = variableDefinitions(); 470 _directives = directives(false); 471 } 472 if (input.charCodeAt(idx) === 123 /*'{'*/) { 473 idx++; 474 ignored(); 475 return { 476 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 477 operation: operation || ('query' as OperationTypeNode.QUERY), 478 name: _name ? { kind: 'Name' as Kind.NAME, value: _name } : undefined, 479 variableDefinitions: _variableDefinitions, 480 directives: _directives, 481 selectionSet: selectionSet(), 482 }; 483 } 484} 485 486function document(input: string, noLoc: boolean): ast.DocumentNode { 487 let match: string | undefined; 488 let definition: ast.OperationDefinitionNode | undefined; 489 ignored(); 490 const definitions: ast.ExecutableDefinitionNode[] = []; 491 do { 492 if ((match = advance(definitionRe)) === 'fragment') { 493 ignored(); 494 definitions.push(fragmentDefinition()); 495 } else if ((definition = operationDefinition(match as OperationTypeNode)) != null) { 496 definitions.push(definition); 497 } else { 498 throw error('Document'); 499 } 500 } while (idx < input.length); 501 502 if (!noLoc) { 503 let loc: Location | undefined; 504 return { 505 kind: 'Document' as Kind.DOCUMENT, 506 definitions, 507 /* v8 ignore start */ 508 set loc(_loc: Location) { 509 loc = _loc; 510 }, 511 /* v8 ignore stop */ 512 // @ts-ignore 513 get loc() { 514 if (!loc) { 515 loc = { 516 start: 0, 517 end: input.length, 518 startToken: undefined, 519 endToken: undefined, 520 source: { 521 body: input, 522 name: 'graphql.web', 523 locationOffset: { line: 1, column: 1 }, 524 }, 525 }; 526 } 527 return loc; 528 }, 529 }; 530 } 531 532 return { 533 kind: 'Document' as Kind.DOCUMENT, 534 definitions, 535 }; 536} 537 538type ParseOptions = { 539 [option: string]: any; 540}; 541 542export function parse( 543 string: string | Source, 544 options?: ParseOptions | undefined 545): ast.DocumentNode { 546 input = typeof string.body === 'string' ? string.body : string; 547 idx = 0; 548 return document(input, options && options.noLocation); 549} 550 551export function parseValue( 552 string: string | Source, 553 _options?: ParseOptions | undefined 554): ast.ValueNode { 555 input = typeof string.body === 'string' ? string.body : string; 556 idx = 0; 557 ignored(); 558 return value(false); 559} 560 561export function parseType( 562 string: string | Source, 563 _options?: ParseOptions | undefined 564): ast.TypeNode { 565 input = typeof string.body === 'string' ? string.body : string; 566 idx = 0; 567 return type(); 568}