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