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