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 type { Kind, OperationTypeNode } from './kind';
8import { GraphQLError } from './error';
9import type { Location, 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;
28function 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 /*'#'*/)
64 while ((char = input.charCodeAt(idx++) | 0) && char !== 10 && char !== 13);
65 }
66 idx--;
67}
68
69function name(): string {
70 const start = idx;
71 for (
72 let char = input.charCodeAt(idx++) | 0;
73 (char >= 48 /*'0'*/ && char <= 57) /*'9'*/ ||
74 (char >= 65 /*'A'*/ && char <= 90) /*'Z'*/ ||
75 char === 95 /*'_'*/ ||
76 (char >= 97 /*'a'*/ && char <= 122) /*'z'*/;
77 char = input.charCodeAt(idx++) | 0
78 );
79 if (start === idx - 1) throw error('Name');
80 const value = input.slice(start, --idx);
81 ignored();
82 return value;
83}
84
85function nameNode(): ast.NameNode {
86 return {
87 kind: 'Name' as Kind.NAME,
88 value: name(),
89 };
90}
91
92const restBlockStringRe = /(?:"""|(?:[\s\S]*?[^\\])""")/y;
93const floatPartRe = /(?:(?:\.\d+)?[eE][+-]?\d+|\.\d+)/y;
94
95function value(constant: true): ast.ConstValueNode;
96function value(constant: boolean): ast.ValueNode;
97
98function value(constant: boolean): ast.ValueNode {
99 let match: string | undefined;
100 switch (input.charCodeAt(idx)) {
101 case 91: // '['
102 idx++;
103 ignored();
104 const values: ast.ValueNode[] = [];
105 while (input.charCodeAt(idx) !== 93 /*']'*/) values.push(value(constant));
106 idx++;
107 ignored();
108 return {
109 kind: 'ListValue' as Kind.LIST,
110 values,
111 };
112
113 case 123: // '{'
114 idx++;
115 ignored();
116 const fields: ast.ObjectFieldNode[] = [];
117 while (input.charCodeAt(idx) !== 125 /*'}'*/) {
118 const name = nameNode();
119 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField');
120 ignored();
121 fields.push({
122 kind: 'ObjectField' as Kind.OBJECT_FIELD,
123 name,
124 value: value(constant),
125 });
126 }
127 idx++;
128 ignored();
129 return {
130 kind: 'ObjectValue' as Kind.OBJECT,
131 fields,
132 };
133
134 case 36: // '$'
135 if (constant) throw error('Variable');
136 idx++;
137 return {
138 kind: 'Variable' as Kind.VARIABLE,
139 name: nameNode(),
140 };
141
142 case 34: // '"'
143 if (input.charCodeAt(idx + 1) === 34 && input.charCodeAt(idx + 2) === 34) {
144 idx += 3;
145 if ((match = advance(restBlockStringRe)) == null) throw error('StringValue');
146 ignored();
147 return {
148 kind: 'StringValue' as Kind.STRING,
149 value: blockString(match.slice(0, -3)),
150 block: true,
151 };
152 } else {
153 const start = idx;
154 idx++;
155 let char: number;
156 let isComplex = false;
157 for (
158 char = input.charCodeAt(idx++) | 0;
159 (char === 92 /*'\\'*/ && (idx++, (isComplex = true))) ||
160 (char !== 10 /*'\n'*/ && char !== 13 /*'\r'*/ && char !== 34 /*'"'*/ && char);
161 char = input.charCodeAt(idx++) | 0
162 );
163 if (char !== 34) throw error('StringValue');
164 match = input.slice(start, idx);
165 ignored();
166 return {
167 kind: 'StringValue' as Kind.STRING,
168 value: isComplex ? (JSON.parse(match) as string) : match.slice(1, -1),
169 block: false,
170 };
171 }
172
173 case 45: // '-'
174 case 48: // '0'
175 case 49: // '1'
176 case 50: // '2'
177 case 51: // '3'
178 case 52: // '4'
179 case 53: // '5'
180 case 54: // '6'
181 case 55: // '7'
182 case 56: // '8'
183 case 57: // '9'
184 const start = idx++;
185 let char: number;
186 while ((char = input.charCodeAt(idx++) | 0) >= 48 /*'0'*/ && char <= 57 /*'9'*/);
187 const intPart = input.slice(start, --idx);
188 if (
189 (char = input.charCodeAt(idx)) === 46 /*'.'*/ ||
190 char === 69 /*'E'*/ ||
191 char === 101 /*'e'*/
192 ) {
193 if ((match = advance(floatPartRe)) == null) throw error('FloatValue');
194 ignored();
195 return {
196 kind: 'FloatValue' as Kind.FLOAT,
197 value: intPart + match,
198 };
199 } else {
200 ignored();
201 return {
202 kind: 'IntValue' as Kind.INT,
203 value: intPart,
204 };
205 }
206
207 case 110: // 'n'
208 if (
209 input.charCodeAt(idx + 1) === 117 &&
210 input.charCodeAt(idx + 2) === 108 &&
211 input.charCodeAt(idx + 3) === 108
212 ) {
213 idx += 4;
214 ignored();
215 return { kind: 'NullValue' as Kind.NULL };
216 } else break;
217
218 case 116: // 't'
219 if (
220 input.charCodeAt(idx + 1) === 114 &&
221 input.charCodeAt(idx + 2) === 117 &&
222 input.charCodeAt(idx + 3) === 101
223 ) {
224 idx += 4;
225 ignored();
226 return { kind: 'BooleanValue' as Kind.BOOLEAN, value: true };
227 } else break;
228
229 case 102: // 'f'
230 if (
231 input.charCodeAt(idx + 1) === 97 &&
232 input.charCodeAt(idx + 2) === 108 &&
233 input.charCodeAt(idx + 3) === 115 &&
234 input.charCodeAt(idx + 4) === 101
235 ) {
236 idx += 5;
237 ignored();
238 return { kind: 'BooleanValue' as Kind.BOOLEAN, value: false };
239 } else break;
240 }
241
242 return {
243 kind: 'EnumValue' as Kind.ENUM,
244 value: name(),
245 };
246}
247
248function arguments_(constant: boolean): ast.ArgumentNode[] | undefined {
249 if (input.charCodeAt(idx) === 40 /*'('*/) {
250 const args: ast.ArgumentNode[] = [];
251 idx++;
252 ignored();
253 do {
254 const name = nameNode();
255 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument');
256 ignored();
257 args.push({
258 kind: 'Argument' as Kind.ARGUMENT,
259 name,
260 value: value(constant),
261 });
262 } while (input.charCodeAt(idx) !== 41 /*')'*/);
263 idx++;
264 ignored();
265 return args;
266 }
267}
268
269function directives(constant: true): ast.ConstDirectiveNode[] | undefined;
270function directives(constant: boolean): ast.DirectiveNode[] | undefined;
271
272function directives(constant: boolean): ast.DirectiveNode[] | undefined {
273 if (input.charCodeAt(idx) === 64 /*'@'*/) {
274 const directives: ast.DirectiveNode[] = [];
275 do {
276 idx++;
277 directives.push({
278 kind: 'Directive' as Kind.DIRECTIVE,
279 name: nameNode(),
280 arguments: arguments_(constant),
281 });
282 } while (input.charCodeAt(idx) === 64 /*'@'*/);
283 return directives;
284 }
285}
286
287function type(): ast.TypeNode {
288 let lists = 0;
289 while (input.charCodeAt(idx) === 91 /*'['*/) {
290 lists++;
291 idx++;
292 ignored();
293 }
294 let type: ast.TypeNode = {
295 kind: 'NamedType' as Kind.NAMED_TYPE,
296 name: nameNode(),
297 };
298 do {
299 if (input.charCodeAt(idx) === 33 /*'!'*/) {
300 idx++;
301 ignored();
302 type = {
303 kind: 'NonNullType' as Kind.NON_NULL_TYPE,
304 type: type as ast.NamedTypeNode | ast.ListTypeNode,
305 } satisfies ast.NonNullTypeNode;
306 }
307 if (lists) {
308 if (input.charCodeAt(idx++) !== 93 /*']'*/) throw error('NamedType');
309 ignored();
310 type = {
311 kind: 'ListType' as Kind.LIST_TYPE,
312 type: type as ast.NamedTypeNode | ast.ListTypeNode,
313 } satisfies ast.ListTypeNode;
314 }
315 } while (lists--);
316 return type;
317}
318
319function selectionSetStart(): ast.SelectionSetNode {
320 if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('SelectionSet');
321 ignored();
322 return selectionSet();
323}
324
325function selectionSet(): ast.SelectionSetNode {
326 const selections: ast.SelectionNode[] = [];
327 do {
328 if (input.charCodeAt(idx) === 46 /*'.'*/) {
329 if (input.charCodeAt(++idx) !== 46 /*'.'*/ || input.charCodeAt(++idx) !== 46 /*'.'*/)
330 throw error('SelectionSet');
331 idx++;
332 ignored();
333 switch (input.charCodeAt(idx)) {
334 case 64 /*'@'*/:
335 selections.push({
336 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT,
337 typeCondition: undefined,
338 directives: directives(false),
339 selectionSet: selectionSetStart(),
340 });
341 break;
342
343 case 111 /*'o'*/:
344 if (input.charCodeAt(idx + 1) === 110 /*'n'*/) {
345 idx += 2;
346 ignored();
347 selections.push({
348 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT,
349 typeCondition: {
350 kind: 'NamedType' as Kind.NAMED_TYPE,
351 name: nameNode(),
352 },
353 directives: directives(false),
354 selectionSet: selectionSetStart(),
355 });
356 } else {
357 selections.push({
358 kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
359 name: nameNode(),
360 directives: directives(false),
361 });
362 }
363 break;
364
365 case 123 /*'{'*/:
366 idx++;
367 ignored();
368 selections.push({
369 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT,
370 typeCondition: undefined,
371 directives: undefined,
372 selectionSet: selectionSet(),
373 });
374 break;
375
376 default:
377 selections.push({
378 kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
379 name: nameNode(),
380 directives: directives(false),
381 });
382 }
383 } else {
384 let name = nameNode();
385 let alias: ast.NameNode | undefined;
386 if (input.charCodeAt(idx) === 58 /*':'*/) {
387 idx++;
388 ignored();
389 alias = name;
390 name = nameNode();
391 }
392 const _arguments = arguments_(false);
393 const _directives = directives(false);
394 let _selectionSet: ast.SelectionSetNode | undefined;
395 if (input.charCodeAt(idx) === 123 /*'{'*/) {
396 idx++;
397 ignored();
398 _selectionSet = selectionSet();
399 }
400 selections.push({
401 kind: 'Field' as Kind.FIELD,
402 alias,
403 name,
404 arguments: _arguments,
405 directives: _directives,
406 selectionSet: _selectionSet,
407 });
408 }
409 } while (input.charCodeAt(idx) !== 125 /*'}'*/);
410 idx++;
411 ignored();
412 return {
413 kind: 'SelectionSet' as Kind.SELECTION_SET,
414 selections,
415 };
416}
417
418function variableDefinitions(): ast.VariableDefinitionNode[] | undefined {
419 ignored();
420 if (input.charCodeAt(idx) === 40 /*'('*/) {
421 const vars: ast.VariableDefinitionNode[] = [];
422 idx++;
423 ignored();
424 do {
425 let _description: ast.StringValueNode | undefined;
426 if (input.charCodeAt(idx) === 34 /*'"'*/) {
427 _description = value(true) as ast.StringValueNode;
428 }
429 if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable');
430 const name = nameNode();
431 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition');
432 ignored();
433 const _type = type();
434 let _defaultValue: ast.ConstValueNode | undefined;
435 if (input.charCodeAt(idx) === 61 /*'='*/) {
436 idx++;
437 ignored();
438 _defaultValue = value(true);
439 }
440 ignored();
441 const varDef: ast.VariableDefinitionNode = {
442 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION,
443 variable: {
444 kind: 'Variable' as Kind.VARIABLE,
445 name,
446 },
447 type: _type,
448 defaultValue: _defaultValue,
449 directives: directives(true),
450 };
451 if (_description) {
452 varDef.description = _description;
453 }
454 vars.push(varDef);
455 } while (input.charCodeAt(idx) !== 41 /*')'*/);
456 idx++;
457 ignored();
458 return vars;
459 }
460}
461
462function fragmentDefinition(description?: ast.StringValueNode): ast.FragmentDefinitionNode {
463 const name = nameNode();
464 if (input.charCodeAt(idx++) !== 111 /*'o'*/ || input.charCodeAt(idx++) !== 110 /*'n'*/)
465 throw error('FragmentDefinition');
466 ignored();
467 const fragDef: ast.FragmentDefinitionNode = {
468 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION,
469 name,
470 typeCondition: {
471 kind: 'NamedType' as Kind.NAMED_TYPE,
472 name: nameNode(),
473 },
474 directives: directives(false),
475 selectionSet: selectionSetStart(),
476 };
477 if (description) {
478 fragDef.description = description;
479 }
480 return fragDef;
481}
482
483function definitions(): ast.DefinitionNode[] {
484 const _definitions: ast.ExecutableDefinitionNode[] = [];
485 do {
486 let _description: ast.StringValueNode | undefined;
487 if (input.charCodeAt(idx) === 34 /*'"'*/) {
488 _description = value(true) as ast.StringValueNode;
489 }
490 if (input.charCodeAt(idx) === 123 /*'{'*/) {
491 // Anonymous operations can't have descriptions
492 if (_description) throw error('Document');
493 idx++;
494 ignored();
495 _definitions.push({
496 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION,
497 operation: 'query' as OperationTypeNode.QUERY,
498 name: undefined,
499 variableDefinitions: undefined,
500 directives: undefined,
501 selectionSet: selectionSet(),
502 });
503 } else {
504 const definition = name();
505 switch (definition) {
506 case 'fragment':
507 _definitions.push(fragmentDefinition(_description));
508 break;
509 case 'query':
510 case 'mutation':
511 case 'subscription':
512 let char: number;
513 let name: ast.NameNode | undefined;
514 if (
515 (char = input.charCodeAt(idx)) !== 40 /*'('*/ &&
516 char !== 64 /*'@'*/ &&
517 char !== 123 /*'{'*/
518 ) {
519 name = nameNode();
520 }
521 const opDef: ast.OperationDefinitionNode = {
522 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION,
523 operation: definition as OperationTypeNode,
524 name,
525 variableDefinitions: variableDefinitions(),
526 directives: directives(false),
527 selectionSet: selectionSetStart(),
528 };
529 if (_description) {
530 opDef.description = _description;
531 }
532 _definitions.push(opDef);
533 break;
534 default:
535 throw error('Document');
536 }
537 }
538 } while (idx < input.length);
539 return _definitions;
540}
541
542type ParseOptions = {
543 [option: string]: any;
544};
545
546export function parse(
547 string: string | Source,
548 options?: ParseOptions | undefined
549): ast.DocumentNode {
550 input = string.body ? string.body : string;
551 idx = 0;
552 ignored();
553 if (options && options.noLocation) {
554 return {
555 kind: 'Document' as Kind.DOCUMENT,
556 definitions: definitions(),
557 };
558 } else {
559 return {
560 kind: 'Document' as Kind.DOCUMENT,
561 definitions: definitions(),
562 loc: {
563 start: 0,
564 end: input.length,
565 startToken: undefined,
566 endToken: undefined,
567 source: {
568 body: input,
569 name: 'graphql.web',
570 locationOffset: { line: 1, column: 1 },
571 },
572 },
573 } as Location;
574 }
575}
576
577export function parseValue(
578 string: string | Source,
579 _options?: ParseOptions | undefined
580): ast.ValueNode {
581 input = string.body ? string.body : string;
582 idx = 0;
583 ignored();
584 return value(false);
585}
586
587export function parseType(
588 string: string | Source,
589 _options?: ParseOptions | undefined
590): ast.TypeNode {
591 input = string.body ? string.body : string;
592 idx = 0;
593 return type();
594}