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