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