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