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 { Kind, OperationTypeNode } from './kind'; 8import { GraphQLError } from './error'; 9import { 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; 69function name(): ast.NameNode | undefined { 70 let match: string | undefined; 71 if ((match = advance(nameRe))) { 72 return { 73 kind: 'Name' as Kind.NAME, 74 value: match, 75 }; 76 } 77} 78 79// NOTE(Safari10 Quirk): This needs to be wrapped in a non-capturing group 80const constRe = /(?:null|true|false)/y; 81 82const variableRe = /\$[_A-Za-z]\w*/y; 83const intRe = /-?\d+/y; 84 85// NOTE(Safari10 Quirk): This cannot be further simplified 86const floatPartRe = /(?:\.\d+)?[eE][+-]?\d+|\.\d+/y; 87 88const complexStringRe = /\\/g; 89const blockStringRe = /"""(?:[\s\S]*?[^\\])?"""/y; 90const stringRe = /"(?:[^\r\n]*?[^\\])?"/y; 91 92function value(constant: true): ast.ConstValueNode; 93function value(constant: boolean): ast.ValueNode; 94 95function value(constant: boolean): ast.ValueNode | undefined { 96 let out: ast.ValueNode | undefined; 97 let match: string | undefined; 98 if ((match = advance(constRe))) { 99 out = 100 match === 'null' 101 ? { 102 kind: 'NullValue' as Kind.NULL, 103 } 104 : { 105 kind: 'BooleanValue' as Kind.BOOLEAN, 106 value: match === 'true', 107 }; 108 } else if (!constant && (match = advance(variableRe))) { 109 out = { 110 kind: 'Variable' as Kind.VARIABLE, 111 name: { 112 kind: 'Name' as Kind.NAME, 113 value: match.slice(1), 114 }, 115 }; 116 } else if ((match = advance(intRe))) { 117 const intPart = match; 118 if ((match = advance(floatPartRe))) { 119 out = { 120 kind: 'FloatValue' as Kind.FLOAT, 121 value: intPart + match, 122 }; 123 } else { 124 out = { 125 kind: 'IntValue' as Kind.INT, 126 value: intPart, 127 }; 128 } 129 } else if ((match = advance(nameRe))) { 130 out = { 131 kind: 'EnumValue' as Kind.ENUM, 132 value: match, 133 }; 134 } else if ((match = advance(blockStringRe))) { 135 out = { 136 kind: 'StringValue' as Kind.STRING, 137 value: blockString(match.slice(3, -3)), 138 block: true, 139 }; 140 } else if ((match = advance(stringRe))) { 141 out = { 142 kind: 'StringValue' as Kind.STRING, 143 value: complexStringRe.test(match) ? (JSON.parse(match) as string) : match.slice(1, -1), 144 block: false, 145 }; 146 } else if ((out = list(constant) || object(constant))) { 147 return out; 148 } 149 150 ignored(); 151 return out; 152} 153 154function list(constant: boolean): ast.ListValueNode | undefined { 155 let match: ast.ValueNode | undefined; 156 if (input.charCodeAt(idx) === 91 /*'['*/) { 157 idx++; 158 ignored(); 159 const values: ast.ValueNode[] = []; 160 while ((match = value(constant))) values.push(match); 161 if (input.charCodeAt(idx++) !== 93 /*']'*/) throw error('ListValue'); 162 ignored(); 163 return { 164 kind: 'ListValue' as Kind.LIST, 165 values, 166 }; 167 } 168} 169 170function object(constant: boolean): ast.ObjectValueNode | undefined { 171 if (input.charCodeAt(idx) === 123 /*'{'*/) { 172 idx++; 173 ignored(); 174 const fields: ast.ObjectFieldNode[] = []; 175 let _name: ast.NameNode | undefined; 176 while ((_name = name())) { 177 ignored(); 178 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField' as Kind.OBJECT_FIELD); 179 ignored(); 180 const _value = value(constant); 181 if (!_value) throw error('ObjectField'); 182 fields.push({ 183 kind: 'ObjectField' as Kind.OBJECT_FIELD, 184 name: _name, 185 value: _value, 186 }); 187 } 188 if (input.charCodeAt(idx++) !== 125 /*'}'*/) throw error('ObjectValue'); 189 ignored(); 190 return { 191 kind: 'ObjectValue' as Kind.OBJECT, 192 fields, 193 }; 194 } 195} 196 197function arguments_(constant: boolean): ast.ArgumentNode[] { 198 const args: ast.ArgumentNode[] = []; 199 ignored(); 200 if (input.charCodeAt(idx) === 40 /*'('*/) { 201 idx++; 202 ignored(); 203 let _name: ast.NameNode | undefined; 204 while ((_name = name())) { 205 ignored(); 206 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument'); 207 ignored(); 208 const _value = value(constant); 209 if (!_value) throw error('Argument'); 210 args.push({ 211 kind: 'Argument' as Kind.ARGUMENT, 212 name: _name, 213 value: _value, 214 }); 215 } 216 if (!args.length || input.charCodeAt(idx++) !== 41 /*')'*/) throw error('Argument'); 217 ignored(); 218 } 219 return args; 220} 221 222function directives(constant: true): ast.ConstDirectiveNode[]; 223function directives(constant: boolean): ast.DirectiveNode[]; 224 225function directives(constant: boolean): ast.DirectiveNode[] { 226 const directives: ast.DirectiveNode[] = []; 227 ignored(); 228 while (input.charCodeAt(idx) === 64 /*'@'*/) { 229 idx++; 230 const _name = name(); 231 if (!_name) throw error('Directive'); 232 ignored(); 233 directives.push({ 234 kind: 'Directive' as Kind.DIRECTIVE, 235 name: _name, 236 arguments: arguments_(constant), 237 }); 238 } 239 return directives; 240} 241 242function field(): ast.FieldNode | undefined { 243 let _name = name(); 244 if (_name) { 245 ignored(); 246 let _alias: ast.NameNode | undefined; 247 if (input.charCodeAt(idx) === 58 /*':'*/) { 248 idx++; 249 ignored(); 250 _alias = _name; 251 _name = name(); 252 if (!_name) throw error('Field'); 253 ignored(); 254 } 255 return { 256 kind: 'Field' as Kind.FIELD, 257 alias: _alias, 258 name: _name, 259 arguments: arguments_(false), 260 directives: directives(false), 261 selectionSet: selectionSet(), 262 }; 263 } 264} 265 266function type(): ast.TypeNode { 267 let match: ast.NameNode | ast.TypeNode | undefined; 268 ignored(); 269 if (input.charCodeAt(idx) === 91 /*'['*/) { 270 idx++; 271 ignored(); 272 const _type = type(); 273 if (!_type || input.charCodeAt(idx++) !== 93 /*']'*/) throw error('ListType'); 274 match = { 275 kind: 'ListType' as Kind.LIST_TYPE, 276 type: _type, 277 }; 278 } else if ((match = name())) { 279 match = { 280 kind: 'NamedType' as Kind.NAMED_TYPE, 281 name: match, 282 }; 283 } else { 284 throw error('NamedType'); 285 } 286 287 ignored(); 288 if (input.charCodeAt(idx) === 33 /*'!'*/) { 289 idx++; 290 ignored(); 291 return { 292 kind: 'NonNullType' as Kind.NON_NULL_TYPE, 293 type: match, 294 }; 295 } else { 296 return match; 297 } 298} 299 300const typeConditionRe = /on/y; 301function typeCondition(): ast.NamedTypeNode | undefined { 302 if (advance(typeConditionRe)) { 303 ignored(); 304 const _name = name(); 305 if (!_name) throw error('NamedType'); 306 ignored(); 307 return { 308 kind: 'NamedType' as Kind.NAMED_TYPE, 309 name: _name, 310 }; 311 } 312} 313 314const fragmentSpreadRe = /\.\.\./y; 315 316function fragmentSpread(): ast.FragmentSpreadNode | ast.InlineFragmentNode | undefined { 317 if (advance(fragmentSpreadRe)) { 318 ignored(); 319 const _idx = idx; 320 let _name: ast.NameNode | undefined; 321 if ((_name = name()) && _name.value !== 'on') { 322 return { 323 kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, 324 name: _name, 325 directives: directives(false), 326 }; 327 } else { 328 idx = _idx; 329 const _typeCondition = typeCondition(); 330 const _directives = directives(false); 331 const _selectionSet = selectionSet(); 332 if (!_selectionSet) throw error('InlineFragment'); 333 return { 334 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, 335 typeCondition: _typeCondition, 336 directives: _directives, 337 selectionSet: _selectionSet, 338 }; 339 } 340 } 341} 342 343function selectionSet(): ast.SelectionSetNode | undefined { 344 let match: ast.SelectionNode | undefined; 345 ignored(); 346 if (input.charCodeAt(idx) === 123 /*'{'*/) { 347 idx++; 348 ignored(); 349 const selections: ast.SelectionNode[] = []; 350 while ((match = fragmentSpread() || field())) selections.push(match); 351 if (!selections.length || input.charCodeAt(idx++) !== 125 /*'}'*/) throw error('SelectionSet'); 352 ignored(); 353 return { 354 kind: 'SelectionSet' as Kind.SELECTION_SET, 355 selections, 356 }; 357 } 358} 359 360function variableDefinitions(): ast.VariableDefinitionNode[] { 361 let match: string | undefined; 362 const vars: ast.VariableDefinitionNode[] = []; 363 ignored(); 364 if (input.charCodeAt(idx) === 40 /*'('*/) { 365 idx++; 366 ignored(); 367 while ((match = advance(variableRe))) { 368 ignored(); 369 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition'); 370 const _type = type(); 371 let _defaultValue: ast.ValueNode | undefined; 372 if (input.charCodeAt(idx) === 61 /*'='*/) { 373 idx++; 374 ignored(); 375 _defaultValue = value(true); 376 if (!_defaultValue) throw error('VariableDefinition'); 377 } 378 ignored(); 379 vars.push({ 380 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION, 381 variable: { 382 kind: 'Variable' as Kind.VARIABLE, 383 name: { 384 kind: 'Name' as Kind.NAME, 385 value: match.slice(1), 386 }, 387 }, 388 type: _type, 389 defaultValue: _defaultValue as ast.ConstValueNode, 390 directives: directives(true), 391 }); 392 } 393 if (input.charCodeAt(idx++) !== 41 /*')'*/) throw error('VariableDefinition'); 394 ignored(); 395 } 396 return vars; 397} 398 399const fragmentDefinitionRe = /fragment/y; 400function fragmentDefinition(): ast.FragmentDefinitionNode | undefined { 401 if (advance(fragmentDefinitionRe)) { 402 ignored(); 403 const _name = name(); 404 if (!_name) throw error('FragmentDefinition'); 405 ignored(); 406 const _typeCondition = typeCondition(); 407 if (!_typeCondition) throw error('FragmentDefinition'); 408 const _directives = directives(false); 409 const _selectionSet = selectionSet(); 410 if (!_selectionSet) throw error('FragmentDefinition'); 411 return { 412 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, 413 name: _name, 414 typeCondition: _typeCondition, 415 directives: _directives, 416 selectionSet: _selectionSet, 417 }; 418 } 419} 420 421// NOTE(Safari10 Quirk): This *might* need to be wrapped in a group, but worked without it too 422const operationDefinitionRe = /(?:query|mutation|subscription)/y; 423 424function operationDefinition(): ast.OperationDefinitionNode | undefined { 425 let _operation: string | undefined; 426 let _name: ast.NameNode | undefined; 427 let _variableDefinitions: ast.VariableDefinitionNode[] = []; 428 let _directives: ast.DirectiveNode[] = []; 429 if ((_operation = advance(operationDefinitionRe))) { 430 ignored(); 431 _name = name(); 432 _variableDefinitions = variableDefinitions(); 433 _directives = directives(false); 434 } 435 const _selectionSet = selectionSet(); 436 if (_selectionSet) { 437 return { 438 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 439 operation: (_operation || 'query') as OperationTypeNode, 440 name: _name, 441 variableDefinitions: _variableDefinitions, 442 directives: _directives, 443 selectionSet: _selectionSet, 444 }; 445 } 446} 447 448function document(): ast.DocumentNode { 449 let match: ast.ExecutableDefinitionNode | void; 450 ignored(); 451 const definitions: ast.ExecutableDefinitionNode[] = []; 452 while ((match = fragmentDefinition() || operationDefinition())) definitions.push(match); 453 return { 454 kind: 'Document' as Kind.DOCUMENT, 455 definitions, 456 }; 457} 458 459type ParseOptions = { 460 [option: string]: any; 461}; 462 463export function parse( 464 string: string | Source, 465 _options?: ParseOptions | undefined 466): ast.DocumentNode { 467 input = typeof string.body === 'string' ? string.body : string; 468 idx = 0; 469 return document(); 470} 471 472export function parseValue( 473 string: string | Source, 474 _options?: ParseOptions | undefined 475): ast.ValueNode { 476 input = typeof string.body === 'string' ? string.body : string; 477 idx = 0; 478 ignored(); 479 const _value = value(false); 480 if (!_value) throw error('ValueNode'); 481 return _value; 482} 483 484export function parseType( 485 string: string | Source, 486 _options?: ParseOptions | undefined 487): ast.TypeNode { 488 input = typeof string.body === 'string' ? string.body : string; 489 idx = 0; 490 return type(); 491}