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