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