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) ? (JSON.parse(match) as string) : match.slice(1, -1), 122 block: false, 123 }; 124 } else if ((out = list(constant) || object(constant))) { 125 return out; 126 } 127 128 ignored(); 129 return out; 130} 131 132function list(constant: boolean): ast.ListValueNode | undefined { 133 let match: ast.ValueNode | undefined; 134 if (input.charCodeAt(idx) === 91 /*'['*/) { 135 idx++; 136 ignored(); 137 const values: ast.ValueNode[] = []; 138 while ((match = value(constant))) values.push(match); 139 if (input.charCodeAt(idx++) !== 93 /*']'*/) throw error(Kind.LIST); 140 ignored(); 141 return { 142 kind: Kind.LIST, 143 values, 144 }; 145 } 146} 147 148function object(constant: boolean): ast.ObjectValueNode | undefined { 149 if (input.charCodeAt(idx) === 123 /*'{'*/) { 150 idx++; 151 ignored(); 152 const fields: ast.ObjectFieldNode[] = []; 153 let _name: ast.NameNode | undefined; 154 while ((_name = name())) { 155 ignored(); 156 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error(Kind.OBJECT_FIELD); 157 ignored(); 158 const _value = value(constant); 159 if (!_value) throw error(Kind.OBJECT_FIELD); 160 fields.push({ 161 kind: Kind.OBJECT_FIELD, 162 name: _name, 163 value: _value, 164 }); 165 } 166 if (input.charCodeAt(idx++) !== 125 /*'}'*/) throw error(Kind.OBJECT); 167 ignored(); 168 return { 169 kind: Kind.OBJECT, 170 fields, 171 }; 172 } 173} 174 175function arguments_(constant: boolean): ast.ArgumentNode[] { 176 const args: ast.ArgumentNode[] = []; 177 ignored(); 178 if (input.charCodeAt(idx) === 40 /*'('*/) { 179 idx++; 180 ignored(); 181 let _name: ast.NameNode | undefined; 182 while ((_name = name())) { 183 ignored(); 184 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error(Kind.ARGUMENT); 185 ignored(); 186 const _value = value(constant); 187 if (!_value) throw error(Kind.ARGUMENT); 188 args.push({ 189 kind: Kind.ARGUMENT, 190 name: _name, 191 value: _value, 192 }); 193 } 194 if (!args.length || input.charCodeAt(idx++) !== 41 /*')'*/) throw error(Kind.ARGUMENT); 195 ignored(); 196 } 197 return args; 198} 199 200function directives(constant: true): ast.ConstDirectiveNode[]; 201function directives(constant: boolean): ast.DirectiveNode[]; 202 203function directives(constant: boolean): ast.DirectiveNode[] { 204 const directives: ast.DirectiveNode[] = []; 205 ignored(); 206 while (input.charCodeAt(idx) === 64 /*'@'*/) { 207 idx++; 208 const _name = name(); 209 if (!_name) throw error(Kind.DIRECTIVE); 210 ignored(); 211 directives.push({ 212 kind: Kind.DIRECTIVE, 213 name: _name, 214 arguments: arguments_(constant), 215 }); 216 } 217 return directives; 218} 219 220function field(): ast.FieldNode | undefined { 221 ignored(); 222 let _name = name(); 223 if (_name) { 224 ignored(); 225 let _alias: ast.NameNode | undefined; 226 if (input.charCodeAt(idx) === 58 /*':'*/) { 227 idx++; 228 ignored(); 229 _alias = _name; 230 _name = name(); 231 if (!_name) throw error(Kind.FIELD); 232 ignored(); 233 } 234 return { 235 kind: Kind.FIELD, 236 alias: _alias, 237 name: _name, 238 arguments: arguments_(false), 239 directives: directives(false), 240 selectionSet: selectionSet(), 241 }; 242 } 243} 244 245function type(): ast.TypeNode | undefined { 246 let match: ast.NameNode | ast.TypeNode | undefined; 247 ignored(); 248 if (input.charCodeAt(idx) === 91 /*'['*/) { 249 idx++; 250 ignored(); 251 const _type = type(); 252 if (!_type || input.charCodeAt(idx++) !== 93 /*']'*/) throw error(Kind.LIST_TYPE); 253 match = { 254 kind: Kind.LIST_TYPE, 255 type: _type, 256 }; 257 } else if ((match = name())) { 258 match = { 259 kind: Kind.NAMED_TYPE, 260 name: match, 261 }; 262 } else { 263 throw error(Kind.NAMED_TYPE); 264 } 265 266 ignored(); 267 if (input.charCodeAt(idx) === 33 /*'!'*/) { 268 idx++; 269 ignored(); 270 return { 271 kind: Kind.NON_NULL_TYPE, 272 type: match, 273 }; 274 } else { 275 return match; 276 } 277} 278 279const typeConditionRe = /on/y; 280function typeCondition(): ast.NamedTypeNode | undefined { 281 if (advance(typeConditionRe)) { 282 ignored(); 283 const _name = name(); 284 if (!_name) throw error(Kind.NAMED_TYPE); 285 ignored(); 286 return { 287 kind: Kind.NAMED_TYPE, 288 name: _name, 289 }; 290 } 291} 292 293const fragmentSpreadRe = /\.\.\./y; 294 295function fragmentSpread(): ast.FragmentSpreadNode | ast.InlineFragmentNode | undefined { 296 ignored(); 297 if (advance(fragmentSpreadRe)) { 298 ignored(); 299 const _idx = idx; 300 let _name: ast.NameNode | undefined; 301 if ((_name = name()) && _name.value !== 'on') { 302 return { 303 kind: Kind.FRAGMENT_SPREAD, 304 name: _name, 305 directives: directives(false), 306 }; 307 } else { 308 idx = _idx; 309 const _typeCondition = typeCondition(); 310 const _directives = directives(false); 311 const _selectionSet = selectionSet(); 312 if (!_selectionSet) throw error(Kind.INLINE_FRAGMENT); 313 return { 314 kind: Kind.INLINE_FRAGMENT, 315 typeCondition: _typeCondition, 316 directives: _directives, 317 selectionSet: _selectionSet, 318 }; 319 } 320 } 321} 322 323function selectionSet(): ast.SelectionSetNode | undefined { 324 let match: ast.SelectionNode | undefined; 325 ignored(); 326 if (input.charCodeAt(idx) === 123 /*'{'*/) { 327 idx++; 328 ignored(); 329 const selections: ast.SelectionNode[] = []; 330 while ((match = fragmentSpread() || field())) selections.push(match); 331 if (!selections.length || input.charCodeAt(idx++) !== 125 /*'}'*/) 332 throw error(Kind.SELECTION_SET); 333 ignored(); 334 return { 335 kind: Kind.SELECTION_SET, 336 selections, 337 }; 338 } 339} 340 341function variableDefinitions(): ast.VariableDefinitionNode[] { 342 let match: string | undefined; 343 const vars: ast.VariableDefinitionNode[] = []; 344 ignored(); 345 if (input.charCodeAt(idx) === 40 /*'('*/) { 346 idx++; 347 ignored(); 348 while ((match = advance(variableRe))) { 349 ignored(); 350 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error(Kind.VARIABLE_DEFINITION); 351 const _type = type(); 352 if (!_type) throw error(Kind.VARIABLE_DEFINITION); 353 let _defaultValue: ast.ValueNode | undefined; 354 if (input.charCodeAt(idx) === 61 /*'='*/) { 355 idx++; 356 ignored(); 357 _defaultValue = value(true); 358 if (!_defaultValue) throw error(Kind.VARIABLE_DEFINITION); 359 } 360 ignored(); 361 vars.push({ 362 kind: Kind.VARIABLE_DEFINITION, 363 variable: { 364 kind: Kind.VARIABLE, 365 name: { 366 kind: Kind.NAME, 367 value: match.slice(1), 368 }, 369 }, 370 type: _type, 371 defaultValue: _defaultValue as ast.ConstValueNode, 372 directives: directives(true), 373 }); 374 } 375 if (input.charCodeAt(idx++) !== 41 /*')'*/) throw error(Kind.VARIABLE_DEFINITION); 376 ignored(); 377 } 378 return vars; 379} 380 381const fragmentDefinitionRe = /fragment/y; 382function fragmentDefinition(): ast.FragmentDefinitionNode | undefined { 383 if (advance(fragmentDefinitionRe)) { 384 ignored(); 385 const _name = name(); 386 if (!_name) throw error(Kind.FRAGMENT_DEFINITION); 387 ignored(); 388 const _typeCondition = typeCondition(); 389 if (!_typeCondition) throw error(Kind.FRAGMENT_DEFINITION); 390 const _directives = directives(false); 391 const _selectionSet = selectionSet(); 392 if (!_selectionSet) throw error(Kind.FRAGMENT_DEFINITION); 393 return { 394 kind: Kind.FRAGMENT_DEFINITION, 395 name: _name, 396 typeCondition: _typeCondition, 397 directives: _directives, 398 selectionSet: _selectionSet, 399 }; 400 } 401} 402 403const operationDefinitionRe = /query|mutation|subscription/y; 404function operationDefinition(): ast.OperationDefinitionNode | undefined { 405 let _operation: string | undefined; 406 let _name: ast.NameNode | undefined; 407 let _variableDefinitions: ast.VariableDefinitionNode[] = []; 408 let _directives: ast.DirectiveNode[] = []; 409 if ((_operation = advance(operationDefinitionRe))) { 410 ignored(); 411 _name = name(); 412 _variableDefinitions = variableDefinitions(); 413 _directives = directives(false); 414 } 415 const _selectionSet = selectionSet(); 416 if (_selectionSet) { 417 return { 418 kind: Kind.OPERATION_DEFINITION, 419 operation: (_operation || 'query') as ast.OperationTypeNode, 420 name: _name, 421 variableDefinitions: _variableDefinitions, 422 directives: _directives, 423 selectionSet: _selectionSet, 424 }; 425 } 426} 427 428function document(): ast.DocumentNode { 429 let match: ast.DefinitionNode | void; 430 ignored(); 431 const definitions: ast.DefinitionNode[] = []; 432 while ((match = fragmentDefinition() || operationDefinition())) definitions.push(match); 433 if (idx !== input.length) throw error(Kind.DOCUMENT); 434 return { 435 kind: Kind.DOCUMENT, 436 definitions, 437 }; 438} 439 440export function parse(string: string): ast.DocumentNode { 441 input = string; 442 idx = 0; 443 return document(); 444} 445 446export function parseValue(string: string): ast.ValueNode | undefined { 447 input = string; 448 idx = 0; 449 ignored(); 450 return value(false); 451} 452 453export function parseType(string: string): ast.TypeNode | undefined { 454 input = string; 455 idx = 0; 456 return type(); 457}