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 { 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 = /\\/g; 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 '(\\.\\.\\.)|' + 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 } 365 const _arguments = arguments_(false); 366 ignored(); 367 const _directives = directives(false); 368 let _selectionSet: ast.SelectionSetNode | undefined; 369 if (input.charCodeAt(idx) === 123 /*'{'*/) { 370 idx++; 371 ignored(); 372 _selectionSet = selectionSet(); 373 } 374 selections.push({ 375 kind: 'Field' as Kind.FIELD, 376 alias: _alias ? { kind: 'Name' as Kind.NAME, value: _alias } : undefined, 377 name: { kind: 'Name' as Kind.NAME, value: match }, 378 arguments: _arguments, 379 directives: _directives, 380 selectionSet: _selectionSet, 381 }); 382 } 383 } else { 384 throw error('SelectionSet'); 385 } 386 } while (input.charCodeAt(idx) !== 125 /*'}'*/); 387 idx++; 388 ignored(); 389 return { 390 kind: 'SelectionSet' as Kind.SELECTION_SET, 391 selections, 392 }; 393} 394 395function variableDefinitions(): ast.VariableDefinitionNode[] | undefined { 396 ignored(); 397 if (input.charCodeAt(idx) === 40 /*'('*/) { 398 const vars: ast.VariableDefinitionNode[] = []; 399 idx++; 400 ignored(); 401 let _name: string | undefined; 402 do { 403 if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable'); 404 if ((_name = advance(nameRe)) == null) throw error('Variable'); 405 ignored(); 406 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition'); 407 ignored(); 408 const _type = type(); 409 let _defaultValue: ast.ConstValueNode | undefined; 410 if (input.charCodeAt(idx) === 61 /*'='*/) { 411 idx++; 412 ignored(); 413 _defaultValue = value(true); 414 } 415 ignored(); 416 vars.push({ 417 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION, 418 variable: { 419 kind: 'Variable' as Kind.VARIABLE, 420 name: { kind: 'Name' as Kind.NAME, value: _name }, 421 }, 422 type: _type, 423 defaultValue: _defaultValue, 424 directives: directives(true), 425 }); 426 } while (input.charCodeAt(idx) !== 41 /*')'*/); 427 idx++; 428 ignored(); 429 return vars; 430 } 431} 432 433function fragmentDefinition(): ast.FragmentDefinitionNode { 434 let _name: string | undefined; 435 let _condition: string | undefined; 436 if ((_name = advance(nameRe)) == null) throw error('FragmentDefinition'); 437 ignored(); 438 if (advance(nameRe) !== 'on') throw error('FragmentDefinition'); 439 ignored(); 440 if ((_condition = advance(nameRe)) == null) throw error('FragmentDefinition'); 441 ignored(); 442 const _directives = directives(false); 443 if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('FragmentDefinition'); 444 ignored(); 445 return { 446 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, 447 name: { kind: 'Name' as Kind.NAME, value: _name }, 448 typeCondition: { 449 kind: 'NamedType' as Kind.NAMED_TYPE, 450 name: { kind: 'Name' as Kind.NAME, value: _condition }, 451 }, 452 directives: _directives, 453 selectionSet: selectionSet(), 454 }; 455} 456 457const definitionRe = /(?:query|mutation|subscription|fragment)/y; 458 459function operationDefinition( 460 operation: OperationTypeNode | undefined 461): ast.OperationDefinitionNode | undefined { 462 let _name: string | undefined; 463 let _variableDefinitions: ast.VariableDefinitionNode[] | undefined; 464 let _directives: ast.DirectiveNode[] | undefined; 465 if (operation) { 466 ignored(); 467 _name = advance(nameRe); 468 _variableDefinitions = variableDefinitions(); 469 _directives = directives(false); 470 } 471 if (input.charCodeAt(idx) === 123 /*'{'*/) { 472 idx++; 473 ignored(); 474 return { 475 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 476 operation: operation || ('query' as OperationTypeNode.QUERY), 477 name: _name ? { kind: 'Name' as Kind.NAME, value: _name } : undefined, 478 variableDefinitions: _variableDefinitions, 479 directives: _directives, 480 selectionSet: selectionSet(), 481 }; 482 } 483} 484 485function document(): ast.DocumentNode { 486 let match: string | undefined; 487 let definition: ast.OperationDefinitionNode | undefined; 488 ignored(); 489 const definitions: ast.ExecutableDefinitionNode[] = []; 490 do { 491 if ((match = advance(definitionRe)) === 'fragment') { 492 ignored(); 493 definitions.push(fragmentDefinition()); 494 } else if ((definition = operationDefinition(match as OperationTypeNode)) != null) { 495 definitions.push(definition); 496 } else { 497 throw error('Document'); 498 } 499 } while (idx < input.length); 500 return { 501 kind: 'Document' as Kind.DOCUMENT, 502 definitions, 503 }; 504} 505 506type ParseOptions = { 507 [option: string]: any; 508}; 509 510export function parse( 511 string: string | Source, 512 _options?: ParseOptions | undefined 513): ast.DocumentNode { 514 input = typeof string.body === 'string' ? string.body : string; 515 idx = 0; 516 return document(); 517} 518 519export function parseValue( 520 string: string | Source, 521 _options?: ParseOptions | undefined 522): ast.ValueNode { 523 input = typeof string.body === 'string' ? string.body : string; 524 idx = 0; 525 ignored(); 526 return value(false); 527} 528 529export function parseType( 530 string: string | Source, 531 _options?: ParseOptions | undefined 532): ast.TypeNode { 533 input = typeof string.body === 'string' ? string.body : string; 534 idx = 0; 535 return type(); 536}