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, OperationTypeNode } from './kind';
8import { GraphQLError } from './error';
9import { 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
68const nameRe = /[_\w][_\d\w]*/y;
69function name(): ast.NameNode | undefined {
70 let match: string | undefined;
71 if ((match = advance(nameRe))) {
72 return {
73 kind: 'Name' as Kind.NAME,
74 value: match,
75 };
76 }
77}
78
79const constRe = /null|true|false/y;
80const variableRe = /\$[_\w][_\d\w]*/y;
81const intRe = /-?\d+/y;
82const floatPartRe = /(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
83const complexStringRe = /\\/g;
84const blockStringRe = /"""(?:[\s\S]+(?="""))?"""/y;
85const stringRe = /"(?:[^"\r\n]+)?"/y;
86
87function value(constant: true): ast.ConstValueNode;
88function value(constant: boolean): ast.ValueNode;
89
90function value(constant: boolean): ast.ValueNode | undefined {
91 let out: ast.ValueNode | undefined;
92 let match: string | undefined;
93 if ((match = advance(constRe))) {
94 out =
95 match === 'null'
96 ? {
97 kind: 'NullValue' as Kind.NULL,
98 }
99 : {
100 kind: 'BooleanValue' as Kind.BOOLEAN,
101 value: match === 'true',
102 };
103 } else if (!constant && (match = advance(variableRe))) {
104 out = {
105 kind: 'Variable' as Kind.VARIABLE,
106 name: {
107 kind: 'Name' as Kind.NAME,
108 value: match.slice(1),
109 },
110 };
111 } else if ((match = advance(intRe))) {
112 const intPart = match;
113 if ((match = advance(floatPartRe))) {
114 out = {
115 kind: 'FloatValue' as Kind.FLOAT,
116 value: intPart + match,
117 };
118 } else {
119 out = {
120 kind: 'IntValue' as Kind.INT,
121 value: intPart,
122 };
123 }
124 } else if ((match = advance(nameRe))) {
125 out = {
126 kind: 'EnumValue' as Kind.ENUM,
127 value: match,
128 };
129 } else if ((match = advance(blockStringRe))) {
130 out = {
131 kind: 'StringValue' as Kind.STRING,
132 value: blockString(match.slice(3, -3)),
133 block: true,
134 };
135 } else if ((match = advance(stringRe))) {
136 out = {
137 kind: 'StringValue' as Kind.STRING,
138 value: complexStringRe.test(match) ? (JSON.parse(match) as string) : match.slice(1, -1),
139 block: false,
140 };
141 } else if ((out = list(constant) || object(constant))) {
142 return out;
143 }
144
145 ignored();
146 return out;
147}
148
149function list(constant: boolean): ast.ListValueNode | undefined {
150 let match: ast.ValueNode | undefined;
151 if (input.charCodeAt(idx) === 91 /*'['*/) {
152 idx++;
153 ignored();
154 const values: ast.ValueNode[] = [];
155 while ((match = value(constant))) values.push(match);
156 if (input.charCodeAt(idx++) !== 93 /*']'*/) throw error('ListValue');
157 ignored();
158 return {
159 kind: 'ListValue' as Kind.LIST,
160 values,
161 };
162 }
163}
164
165function object(constant: boolean): ast.ObjectValueNode | undefined {
166 if (input.charCodeAt(idx) === 123 /*'{'*/) {
167 idx++;
168 ignored();
169 const fields: ast.ObjectFieldNode[] = [];
170 let _name: ast.NameNode | undefined;
171 while ((_name = name())) {
172 ignored();
173 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField' as Kind.OBJECT_FIELD);
174 ignored();
175 const _value = value(constant);
176 if (!_value) throw error('ObjectField');
177 fields.push({
178 kind: 'ObjectField' as Kind.OBJECT_FIELD,
179 name: _name,
180 value: _value,
181 });
182 }
183 if (input.charCodeAt(idx++) !== 125 /*'}'*/) throw error('ObjectValue');
184 ignored();
185 return {
186 kind: 'ObjectValue' as Kind.OBJECT,
187 fields,
188 };
189 }
190}
191
192function arguments_(constant: boolean): ast.ArgumentNode[] {
193 const args: ast.ArgumentNode[] = [];
194 ignored();
195 if (input.charCodeAt(idx) === 40 /*'('*/) {
196 idx++;
197 ignored();
198 let _name: ast.NameNode | undefined;
199 while ((_name = name())) {
200 ignored();
201 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument');
202 ignored();
203 const _value = value(constant);
204 if (!_value) throw error('Argument');
205 args.push({
206 kind: 'Argument' as Kind.ARGUMENT,
207 name: _name,
208 value: _value,
209 });
210 }
211 if (!args.length || input.charCodeAt(idx++) !== 41 /*')'*/) throw error('Argument');
212 ignored();
213 }
214 return args;
215}
216
217function directives(constant: true): ast.ConstDirectiveNode[];
218function directives(constant: boolean): ast.DirectiveNode[];
219
220function directives(constant: boolean): ast.DirectiveNode[] {
221 const directives: ast.DirectiveNode[] = [];
222 ignored();
223 while (input.charCodeAt(idx) === 64 /*'@'*/) {
224 idx++;
225 const _name = name();
226 if (!_name) throw error('Directive');
227 ignored();
228 directives.push({
229 kind: 'Directive' as Kind.DIRECTIVE,
230 name: _name,
231 arguments: arguments_(constant),
232 });
233 }
234 return directives;
235}
236
237function field(): ast.FieldNode | undefined {
238 let _name = name();
239 if (_name) {
240 ignored();
241 let _alias: ast.NameNode | undefined;
242 if (input.charCodeAt(idx) === 58 /*':'*/) {
243 idx++;
244 ignored();
245 _alias = _name;
246 _name = name();
247 if (!_name) throw error('Field');
248 ignored();
249 }
250 return {
251 kind: 'Field' as Kind.FIELD,
252 alias: _alias,
253 name: _name,
254 arguments: arguments_(false),
255 directives: directives(false),
256 selectionSet: selectionSet(),
257 };
258 }
259}
260
261function type(): ast.TypeNode {
262 let match: ast.NameNode | ast.TypeNode | undefined;
263 ignored();
264 if (input.charCodeAt(idx) === 91 /*'['*/) {
265 idx++;
266 ignored();
267 const _type = type();
268 if (!_type || input.charCodeAt(idx++) !== 93 /*']'*/) throw error('ListType');
269 match = {
270 kind: 'ListType' as Kind.LIST_TYPE,
271 type: _type,
272 };
273 } else if ((match = name())) {
274 match = {
275 kind: 'NamedType' as Kind.NAMED_TYPE,
276 name: match,
277 };
278 } else {
279 throw error('NamedType');
280 }
281
282 ignored();
283 if (input.charCodeAt(idx) === 33 /*'!'*/) {
284 idx++;
285 ignored();
286 return {
287 kind: 'NonNullType' as Kind.NON_NULL_TYPE,
288 type: match,
289 };
290 } else {
291 return match;
292 }
293}
294
295const typeConditionRe = /on/y;
296function typeCondition(): ast.NamedTypeNode | undefined {
297 if (advance(typeConditionRe)) {
298 ignored();
299 const _name = name();
300 if (!_name) throw error('NamedType');
301 ignored();
302 return {
303 kind: 'NamedType' as Kind.NAMED_TYPE,
304 name: _name,
305 };
306 }
307}
308
309const fragmentSpreadRe = /\.\.\./y;
310
311function fragmentSpread(): ast.FragmentSpreadNode | ast.InlineFragmentNode | undefined {
312 if (advance(fragmentSpreadRe)) {
313 ignored();
314 const _idx = idx;
315 let _name: ast.NameNode | undefined;
316 if ((_name = name()) && _name.value !== 'on') {
317 return {
318 kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
319 name: _name,
320 directives: directives(false),
321 };
322 } else {
323 idx = _idx;
324 const _typeCondition = typeCondition();
325 const _directives = directives(false);
326 const _selectionSet = selectionSet();
327 if (!_selectionSet) throw error('InlineFragment');
328 return {
329 kind: 'InlineFragment' as 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())) selections.push(match);
346 if (!selections.length || input.charCodeAt(idx++) !== 125 /*'}'*/) throw error('SelectionSet');
347 ignored();
348 return {
349 kind: 'SelectionSet' as 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 /*':'*/) throw error('VariableDefinition');
365 const _type = type();
366 let _defaultValue: ast.ValueNode | undefined;
367 if (input.charCodeAt(idx) === 61 /*'='*/) {
368 idx++;
369 ignored();
370 _defaultValue = value(true);
371 if (!_defaultValue) throw error('VariableDefinition');
372 }
373 ignored();
374 vars.push({
375 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION,
376 variable: {
377 kind: 'Variable' as Kind.VARIABLE,
378 name: {
379 kind: 'Name' as Kind.NAME,
380 value: match.slice(1),
381 },
382 },
383 type: _type,
384 defaultValue: _defaultValue as ast.ConstValueNode,
385 directives: directives(true),
386 });
387 }
388 if (input.charCodeAt(idx++) !== 41 /*')'*/) throw error('VariableDefinition');
389 ignored();
390 }
391 return vars;
392}
393
394const fragmentDefinitionRe = /fragment/y;
395function fragmentDefinition(): ast.FragmentDefinitionNode | undefined {
396 if (advance(fragmentDefinitionRe)) {
397 ignored();
398 const _name = name();
399 if (!_name) throw error('FragmentDefinition');
400 ignored();
401 const _typeCondition = typeCondition();
402 if (!_typeCondition) throw error('FragmentDefinition');
403 const _directives = directives(false);
404 const _selectionSet = selectionSet();
405 if (!_selectionSet) throw error('FragmentDefinition');
406 return {
407 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION,
408 name: _name,
409 typeCondition: _typeCondition,
410 directives: _directives,
411 selectionSet: _selectionSet,
412 };
413 }
414}
415
416const operationDefinitionRe = /query|mutation|subscription/y;
417function operationDefinition(): ast.OperationDefinitionNode | undefined {
418 let _operation: string | undefined;
419 let _name: ast.NameNode | undefined;
420 let _variableDefinitions: ast.VariableDefinitionNode[] = [];
421 let _directives: ast.DirectiveNode[] = [];
422 if ((_operation = advance(operationDefinitionRe))) {
423 ignored();
424 _name = name();
425 _variableDefinitions = variableDefinitions();
426 _directives = directives(false);
427 }
428 const _selectionSet = selectionSet();
429 if (_selectionSet) {
430 return {
431 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION,
432 operation: (_operation || 'query') as OperationTypeNode,
433 name: _name,
434 variableDefinitions: _variableDefinitions,
435 directives: _directives,
436 selectionSet: _selectionSet,
437 };
438 }
439}
440
441function document(): ast.DocumentNode {
442 let match: ast.ExecutableDefinitionNode | void;
443 ignored();
444 const definitions: ast.ExecutableDefinitionNode[] = [];
445 while ((match = fragmentDefinition() || operationDefinition())) definitions.push(match);
446 return {
447 kind: 'Document' as Kind.DOCUMENT,
448 definitions,
449 };
450}
451
452type ParseOptions = {
453 [option: string]: any;
454};
455
456export function parse(
457 string: string | Source,
458 _options?: ParseOptions | undefined
459): ast.DocumentNode {
460 input = typeof string.body === 'string' ? string.body : string;
461 idx = 0;
462 return document();
463}
464
465export function parseValue(
466 string: string | Source,
467 _options?: ParseOptions | undefined
468): ast.ValueNode {
469 input = typeof string.body === 'string' ? string.body : string;
470 idx = 0;
471 ignored();
472 const _value = value(false);
473 if (!_value) throw error('ValueNode');
474 return _value;
475}
476
477export function parseType(
478 string: string | Source,
479 _options?: ParseOptions | undefined
480): ast.TypeNode {
481 input = typeof string.body === 'string' ? string.body : string;
482 idx = 0;
483 return type();
484}