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