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