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);
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 let _name: ast.NameNode | undefined;
314 if (_name = name()) {
315 return {
316 kind: Kind.FRAGMENT_SPREAD,
317 name: _name,
318 directives: directives(false),
319 };
320 } else {
321 const _typeCondition = typeCondition();
322 const _directives = directives(false);
323 const _selectionSet = selectionSet();
324 if (!_selectionSet)
325 throw error(Kind.INLINE_FRAGMENT);
326 return {
327 kind: Kind.INLINE_FRAGMENT,
328 typeCondition: _typeCondition,
329 directives: _directives,
330 selectionSet: _selectionSet,
331 };
332 }
333 }
334}
335
336function selectionSet(): ast.SelectionSetNode | undefined {
337 let match: ast.SelectionNode | undefined;
338 ignored();
339 if (input.charCodeAt(idx) === 123 /*'{'*/) {
340 idx++;
341 ignored();
342 const selections: ast.SelectionNode[] = [];
343 while (match = fragmentSpread() || field())
344 selections.push(match);
345 if (!selections.length || input.charCodeAt(idx++) !== 125 /*'}'*/)
346 throw error(Kind.SELECTION_SET);
347 ignored();
348 return {
349 kind: Kind.SELECTION_SET,
350 selections,
351 };
352 }
353}
354
355function variableDefinitions(): ast.VariableDefinitionNode[] {
356 let match: string | undefined;
357 const vars: ast.VariableDefinitionNode[] = [];
358 ignored();
359 if (input.charCodeAt(idx) === 40 /*'('*/) {
360 idx++;
361 ignored();
362 while (match = advance(variableRe)) {
363 ignored();
364 if (input.charCodeAt(idx++) !== 58 /*':'*/)
365 throw error(Kind.VARIABLE_DEFINITION);
366 const _type = type();
367 if (!_type)
368 throw error(Kind.VARIABLE_DEFINITION);
369 let _defaultValue: ast.ValueNode | undefined;
370 if (input.charCodeAt(idx) === 61 /*'='*/) {
371 idx++;
372 ignored();
373 _defaultValue = value(true);
374 if (!_defaultValue)
375 throw error(Kind.VARIABLE_DEFINITION);
376 }
377 ignored();
378 vars.push({
379 kind: Kind.VARIABLE_DEFINITION,
380 variable: {
381 kind: Kind.VARIABLE,
382 name: {
383 kind: Kind.NAME,
384 value: match.slice(1),
385 },
386 },
387 type: _type,
388 defaultValue: _defaultValue as ast.ConstValueNode,
389 directives: directives(true),
390 });
391 }
392 if (input.charCodeAt(idx++) !== 41 /*')'*/)
393 throw error(Kind.VARIABLE_DEFINITION);
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)
405 throw error(Kind.FRAGMENT_DEFINITION);
406 ignored();
407 const _typeCondition = typeCondition();
408 if (!_typeCondition)
409 throw error(Kind.FRAGMENT_DEFINITION);
410 const _directives = directives(false);
411 const _selectionSet = selectionSet();
412 if (!_selectionSet)
413 throw error(Kind.FRAGMENT_DEFINITION);
414 return {
415 kind: Kind.FRAGMENT_DEFINITION,
416 name: _name,
417 typeCondition: _typeCondition,
418 directives: _directives,
419 selectionSet: _selectionSet,
420 };
421 }
422}
423
424const operationDefinitionRe = /query|mutation|subscription/y;
425function operationDefinition(): ast.OperationDefinitionNode | undefined {
426 let _operation: string | undefined;
427 let _name: ast.NameNode | undefined;
428 let _variableDefinitions: ast.VariableDefinitionNode[] = [];
429 let _directives: ast.DirectiveNode[] = [];
430 if (_operation = advance(operationDefinitionRe)) {
431 ignored();
432 _name = name();
433 _variableDefinitions = variableDefinitions();
434 _directives = directives(false);
435 }
436 const _selectionSet = selectionSet();
437 if (_selectionSet) {
438 return {
439 kind: Kind.OPERATION_DEFINITION,
440 operation: (_operation || 'query') as ast.OperationTypeNode,
441 name: _name,
442 variableDefinitions: _variableDefinitions,
443 directives: _directives,
444 selectionSet: _selectionSet,
445 };
446 }
447}
448
449function document(): ast.DocumentNode {
450 let match: ast.DefinitionNode | void;
451 ignored();
452 const definitions: ast.DefinitionNode[] = [];
453 while (match = fragmentDefinition() || operationDefinition())
454 definitions.push(match);
455 if (idx !== input.length)
456 throw error(Kind.DOCUMENT);
457 return {
458 kind: Kind.DOCUMENT,
459 definitions,
460 };
461}
462
463export function parse(string: string): ast.DocumentNode {
464 input = string;
465 idx = 0;
466 return document();
467}
468
469export function parseValue(string: string): ast.ValueNode | undefined {
470 input = string;
471 idx = 0;
472 ignored();
473 return value(false);
474}
475
476export function parseType(string: string): ast.TypeNode | undefined {
477 input = string;
478 idx = 0;
479 return type();
480}