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).replace(/\\"""/g, '"""'); 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 const _idx = idx; 314 let _name: ast.NameNode | undefined; 315 if ((_name = name()) && _name.value !== 'on') { 316 return { 317 kind: Kind.FRAGMENT_SPREAD, 318 name: _name, 319 directives: directives(false), 320 }; 321 } else { 322 idx = _idx; 323 const _typeCondition = typeCondition(); 324 const _directives = directives(false); 325 const _selectionSet = selectionSet(); 326 if (!_selectionSet) 327 throw error(Kind.INLINE_FRAGMENT); 328 return { 329 kind: Kind.INLINE_FRAGMENT, 330 typeCondition: _typeCondition, 331 directives: _directives, 332 selectionSet: _selectionSet, 333 }; 334 } 335 } 336} 337 338function selectionSet(): ast.SelectionSetNode | undefined { 339 let match: ast.SelectionNode | undefined; 340 ignored(); 341 if (input.charCodeAt(idx) === 123 /*'{'*/) { 342 idx++; 343 ignored(); 344 const selections: ast.SelectionNode[] = []; 345 while (match = fragmentSpread() || field()) 346 selections.push(match); 347 if (!selections.length || input.charCodeAt(idx++) !== 125 /*'}'*/) 348 throw error(Kind.SELECTION_SET); 349 ignored(); 350 return { 351 kind: Kind.SELECTION_SET, 352 selections, 353 }; 354 } 355} 356 357function variableDefinitions(): ast.VariableDefinitionNode[] { 358 let match: string | undefined; 359 const vars: ast.VariableDefinitionNode[] = []; 360 ignored(); 361 if (input.charCodeAt(idx) === 40 /*'('*/) { 362 idx++; 363 ignored(); 364 while (match = advance(variableRe)) { 365 ignored(); 366 if (input.charCodeAt(idx++) !== 58 /*':'*/) 367 throw error(Kind.VARIABLE_DEFINITION); 368 const _type = type(); 369 if (!_type) 370 throw error(Kind.VARIABLE_DEFINITION); 371 let _defaultValue: ast.ValueNode | undefined; 372 if (input.charCodeAt(idx) === 61 /*'='*/) { 373 idx++; 374 ignored(); 375 _defaultValue = value(true); 376 if (!_defaultValue) 377 throw error(Kind.VARIABLE_DEFINITION); 378 } 379 ignored(); 380 vars.push({ 381 kind: Kind.VARIABLE_DEFINITION, 382 variable: { 383 kind: Kind.VARIABLE, 384 name: { 385 kind: Kind.NAME, 386 value: match.slice(1), 387 }, 388 }, 389 type: _type, 390 defaultValue: _defaultValue as ast.ConstValueNode, 391 directives: directives(true), 392 }); 393 } 394 if (input.charCodeAt(idx++) !== 41 /*')'*/) 395 throw error(Kind.VARIABLE_DEFINITION); 396 ignored(); 397 } 398 return vars; 399} 400 401const fragmentDefinitionRe = /fragment/y; 402function fragmentDefinition(): ast.FragmentDefinitionNode | undefined { 403 if (advance(fragmentDefinitionRe)) { 404 ignored(); 405 const _name = name(); 406 if (!_name) 407 throw error(Kind.FRAGMENT_DEFINITION); 408 ignored(); 409 const _typeCondition = typeCondition(); 410 if (!_typeCondition) 411 throw error(Kind.FRAGMENT_DEFINITION); 412 const _directives = directives(false); 413 const _selectionSet = selectionSet(); 414 if (!_selectionSet) 415 throw error(Kind.FRAGMENT_DEFINITION); 416 return { 417 kind: Kind.FRAGMENT_DEFINITION, 418 name: _name, 419 typeCondition: _typeCondition, 420 directives: _directives, 421 selectionSet: _selectionSet, 422 }; 423 } 424} 425 426const operationDefinitionRe = /query|mutation|subscription/y; 427function operationDefinition(): ast.OperationDefinitionNode | undefined { 428 let _operation: string | undefined; 429 let _name: ast.NameNode | undefined; 430 let _variableDefinitions: ast.VariableDefinitionNode[] = []; 431 let _directives: ast.DirectiveNode[] = []; 432 if (_operation = advance(operationDefinitionRe)) { 433 ignored(); 434 _name = name(); 435 _variableDefinitions = variableDefinitions(); 436 _directives = directives(false); 437 } 438 const _selectionSet = selectionSet(); 439 if (_selectionSet) { 440 return { 441 kind: Kind.OPERATION_DEFINITION, 442 operation: (_operation || 'query') as ast.OperationTypeNode, 443 name: _name, 444 variableDefinitions: _variableDefinitions, 445 directives: _directives, 446 selectionSet: _selectionSet, 447 }; 448 } 449} 450 451function document(): ast.DocumentNode { 452 let match: ast.DefinitionNode | void; 453 ignored(); 454 const definitions: ast.DefinitionNode[] = []; 455 while (match = fragmentDefinition() || operationDefinition()) 456 definitions.push(match); 457 if (idx !== input.length) 458 throw error(Kind.DOCUMENT); 459 return { 460 kind: Kind.DOCUMENT, 461 definitions, 462 }; 463} 464 465export function parse(string: string): ast.DocumentNode { 466 input = string; 467 idx = 0; 468 return document(); 469} 470 471export function parseValue(string: string): ast.ValueNode | undefined { 472 input = string; 473 idx = 0; 474 ignored(); 475 return value(false); 476} 477 478export function parseType(string: string): ast.TypeNode | undefined { 479 input = string; 480 idx = 0; 481 return type(); 482}