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 type { Kind, OperationTypeNode } from './kind';
8import { GraphQLError } from './error';
9import type { 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;
28function 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 = /[_A-Za-z]\w*/y;
69
70// NOTE: This should be compressed by our build step
71// This merges all possible value parsing into one regular expression
72const valueRe = new RegExp(
73 '(?:' +
74 // `null`, `true`, and `false` literals (BooleanValue & NullValue)
75 '(null|true|false)|' +
76 // Variables starting with `$` then having a name (VariableNode)
77 '\\$(' +
78 nameRe.source +
79 ')|' +
80 // Numbers, starting with int then optionally following with a float part (IntValue and FloatValue)
81 '(-?\\d+)((?:\\.\\d+)?[eE][+-]?\\d+|\\.\\d+)?|' +
82 // Block strings starting with `"""` until the next unescaped `"""` (StringValue)
83 '("""(?:"""|(?:[\\s\\S]*?[^\\\\])"""))|' +
84 // Strings starting with `"` must be on one line (StringValue)
85 '("(?:"|[^\\r\\n]*?[^\\\\]"))|' + // string
86 // Enums are simply names except for our literals (EnumValue)
87 '(' +
88 nameRe.source +
89 '))',
90 'y'
91);
92
93// NOTE: Each of the groups above end up in the RegExpExecArray at the specified indices (starting with 1)
94const enum ValueGroup {
95 Const = 1,
96 Var,
97 Int,
98 Float,
99 BlockString,
100 String,
101 Enum,
102}
103
104type ValueExec = RegExpExecArray & {
105 [Prop in ValueGroup]: string | undefined;
106};
107
108const complexStringRe = /\\/;
109
110function value(constant: true): ast.ConstValueNode;
111function value(constant: boolean): ast.ValueNode;
112
113function value(constant: boolean): ast.ValueNode {
114 let match: string | undefined;
115 let exec: ValueExec | null;
116 valueRe.lastIndex = idx;
117 if (input.charCodeAt(idx) === 91 /*'['*/) {
118 // Lists are checked ahead of time with `[` chars
119 idx++;
120 ignored();
121 const values: ast.ValueNode[] = [];
122 while (input.charCodeAt(idx) !== 93 /*']'*/) values.push(value(constant));
123 idx++;
124 ignored();
125 return {
126 kind: 'ListValue' as Kind.LIST,
127 values,
128 };
129 } else if (input.charCodeAt(idx) === 123 /*'{'*/) {
130 // Objects are checked ahead of time with `{` chars
131 idx++;
132 ignored();
133 const fields: ast.ObjectFieldNode[] = [];
134 while (input.charCodeAt(idx) !== 125 /*'}'*/) {
135 if ((match = advance(nameRe)) == null) throw error('ObjectField');
136 ignored();
137 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField');
138 ignored();
139 fields.push({
140 kind: 'ObjectField' as Kind.OBJECT_FIELD,
141 name: { kind: 'Name' as Kind.NAME, value: match },
142 value: value(constant),
143 });
144 }
145 idx++;
146 ignored();
147 return {
148 kind: 'ObjectValue' as Kind.OBJECT,
149 fields,
150 };
151 } else if ((exec = valueRe.exec(input) as ValueExec) != null) {
152 // Starting from here, the merged `valueRe` is used
153 idx = valueRe.lastIndex;
154 ignored();
155 if ((match = exec[ValueGroup.Const]) != null) {
156 return match === 'null'
157 ? { kind: 'NullValue' as Kind.NULL }
158 : {
159 kind: 'BooleanValue' as Kind.BOOLEAN,
160 value: match === 'true',
161 };
162 } else if ((match = exec[ValueGroup.Var]) != null) {
163 if (constant) {
164 throw error('Variable');
165 } else {
166 return {
167 kind: 'Variable' as Kind.VARIABLE,
168 name: {
169 kind: 'Name' as Kind.NAME,
170 value: match,
171 },
172 };
173 }
174 } else if ((match = exec[ValueGroup.Int]) != null) {
175 let floatPart: string | undefined;
176 if ((floatPart = exec[ValueGroup.Float]) != null) {
177 return {
178 kind: 'FloatValue' as Kind.FLOAT,
179 value: match + floatPart,
180 };
181 } else {
182 return {
183 kind: 'IntValue' as Kind.INT,
184 value: match,
185 };
186 }
187 } else if ((match = exec[ValueGroup.BlockString]) != null) {
188 return {
189 kind: 'StringValue' as Kind.STRING,
190 value: blockString(match.slice(3, -3)),
191 block: true,
192 };
193 } else if ((match = exec[ValueGroup.String]) != null) {
194 return {
195 kind: 'StringValue' as Kind.STRING,
196 // When strings don't contain escape codes, a simple slice will be enough, otherwise
197 // `JSON.parse` matches GraphQL's string parsing perfectly
198 value: complexStringRe.test(match) ? (JSON.parse(match) as string) : match.slice(1, -1),
199 block: false,
200 };
201 } else if ((match = exec[ValueGroup.Enum]) != null) {
202 return {
203 kind: 'EnumValue' as Kind.ENUM,
204 value: match,
205 };
206 }
207 }
208
209 throw error('Value');
210}
211
212function arguments_(constant: boolean): ast.ArgumentNode[] | undefined {
213 if (input.charCodeAt(idx) === 40 /*'('*/) {
214 const args: ast.ArgumentNode[] = [];
215 idx++;
216 ignored();
217 let _name: string | undefined;
218 do {
219 if ((_name = advance(nameRe)) == null) throw error('Argument');
220 ignored();
221 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument');
222 ignored();
223 args.push({
224 kind: 'Argument' as Kind.ARGUMENT,
225 name: { kind: 'Name' as Kind.NAME, value: _name },
226 value: value(constant),
227 });
228 } while (input.charCodeAt(idx) !== 41 /*')'*/);
229 idx++;
230 ignored();
231 return args;
232 }
233}
234
235function directives(constant: true): ast.ConstDirectiveNode[] | undefined;
236function directives(constant: boolean): ast.DirectiveNode[] | undefined;
237
238function directives(constant: boolean): ast.DirectiveNode[] | undefined {
239 if (input.charCodeAt(idx) === 64 /*'@'*/) {
240 const directives: ast.DirectiveNode[] = [];
241 let _name: string | undefined;
242 do {
243 idx++;
244 if ((_name = advance(nameRe)) == null) throw error('Directive');
245 ignored();
246 directives.push({
247 kind: 'Directive' as Kind.DIRECTIVE,
248 name: { kind: 'Name' as Kind.NAME, value: _name },
249 arguments: arguments_(constant),
250 });
251 } while (input.charCodeAt(idx) === 64 /*'@'*/);
252 return directives;
253 }
254}
255
256function type(): ast.TypeNode {
257 let match: string | undefined;
258 let lists = 0;
259 while (input.charCodeAt(idx) === 91 /*'['*/) {
260 lists++;
261 idx++;
262 ignored();
263 }
264 if ((match = advance(nameRe)) == null) throw error('NamedType');
265 ignored();
266 let type: ast.TypeNode = {
267 kind: 'NamedType' as Kind.NAMED_TYPE,
268 name: { kind: 'Name' as Kind.NAME, value: match },
269 };
270 do {
271 if (input.charCodeAt(idx) === 33 /*'!'*/) {
272 idx++;
273 ignored();
274 type = {
275 kind: 'NonNullType' as Kind.NON_NULL_TYPE,
276 type: type as ast.NamedTypeNode | ast.ListTypeNode,
277 } satisfies ast.NonNullTypeNode;
278 }
279 if (lists) {
280 if (input.charCodeAt(idx++) !== 93 /*']'*/) throw error('NamedType');
281 ignored();
282 type = {
283 kind: 'ListType' as Kind.LIST_TYPE,
284 type: type as ast.NamedTypeNode | ast.ListTypeNode,
285 } satisfies ast.ListTypeNode;
286 }
287 } while (lists--);
288 return type;
289}
290
291// NOTE: This should be compressed by our build step
292// This merges the two possible selection parsing branches into one regular expression
293const selectionRe = new RegExp(
294 '(?:' +
295 // fragment spreads (FragmentSpread or InlineFragment nodes)
296 '(\\.{3})|' +
297 // field aliases or names (FieldNode)
298 '(' +
299 nameRe.source +
300 '))',
301 'y'
302);
303
304// NOTE: Each of the groups above end up in the RegExpExecArray at the indices 1&2
305const enum SelectionGroup {
306 Spread = 1,
307 Name,
308}
309
310type SelectionExec = RegExpExecArray & {
311 [Prop in SelectionGroup]: string | undefined;
312};
313
314function selectionSet(): ast.SelectionSetNode {
315 const selections: ast.SelectionNode[] = [];
316 let match: string | undefined;
317 let exec: SelectionExec | null;
318 do {
319 selectionRe.lastIndex = idx;
320 if ((exec = selectionRe.exec(input) as SelectionExec) != null) {
321 idx = selectionRe.lastIndex;
322 if (exec[SelectionGroup.Spread] != null) {
323 ignored();
324 let match = advance(nameRe);
325 if (match != null && match !== 'on') {
326 // A simple `...Name` spread with optional directives
327 ignored();
328 selections.push({
329 kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
330 name: { kind: 'Name' as Kind.NAME, value: match },
331 directives: directives(false),
332 });
333 } else {
334 ignored();
335 if (match === 'on') {
336 // An inline `... on Name` spread; if this doesn't match, the type condition has been omitted
337 if ((match = advance(nameRe)) == null) throw error('NamedType');
338 ignored();
339 }
340 const _directives = directives(false);
341 if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('InlineFragment');
342 ignored();
343 selections.push({
344 kind: 'InlineFragment' as Kind.INLINE_FRAGMENT,
345 typeCondition: match
346 ? {
347 kind: 'NamedType' as Kind.NAMED_TYPE,
348 name: { kind: 'Name' as Kind.NAME, value: match },
349 }
350 : undefined,
351 directives: _directives,
352 selectionSet: selectionSet(),
353 });
354 }
355 } else if ((match = exec[SelectionGroup.Name]) != null) {
356 let _alias: string | undefined;
357 ignored();
358 // Parse the optional alias, by reassigning and then getting the name
359 if (input.charCodeAt(idx) === 58 /*':'*/) {
360 idx++;
361 ignored();
362 _alias = match;
363 if ((match = advance(nameRe)) == null) throw error('Field');
364 ignored();
365 }
366 const _arguments = arguments_(false);
367 ignored();
368 const _directives = directives(false);
369 let _selectionSet: ast.SelectionSetNode | undefined;
370 if (input.charCodeAt(idx) === 123 /*'{'*/) {
371 idx++;
372 ignored();
373 _selectionSet = selectionSet();
374 }
375 selections.push({
376 kind: 'Field' as Kind.FIELD,
377 alias: _alias ? { kind: 'Name' as Kind.NAME, value: _alias } : undefined,
378 name: { kind: 'Name' as Kind.NAME, value: match },
379 arguments: _arguments,
380 directives: _directives,
381 selectionSet: _selectionSet,
382 });
383 }
384 } else {
385 throw error('SelectionSet');
386 }
387 } while (input.charCodeAt(idx) !== 125 /*'}'*/);
388 idx++;
389 ignored();
390 return {
391 kind: 'SelectionSet' as Kind.SELECTION_SET,
392 selections,
393 };
394}
395
396function variableDefinitions(): ast.VariableDefinitionNode[] | undefined {
397 ignored();
398 if (input.charCodeAt(idx) === 40 /*'('*/) {
399 const vars: ast.VariableDefinitionNode[] = [];
400 idx++;
401 ignored();
402 let _name: string | undefined;
403 do {
404 if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable');
405 if ((_name = advance(nameRe)) == null) throw error('Variable');
406 ignored();
407 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition');
408 ignored();
409 const _type = type();
410 let _defaultValue: ast.ConstValueNode | undefined;
411 if (input.charCodeAt(idx) === 61 /*'='*/) {
412 idx++;
413 ignored();
414 _defaultValue = value(true);
415 }
416 ignored();
417 vars.push({
418 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION,
419 variable: {
420 kind: 'Variable' as Kind.VARIABLE,
421 name: { kind: 'Name' as Kind.NAME, value: _name },
422 },
423 type: _type,
424 defaultValue: _defaultValue,
425 directives: directives(true),
426 });
427 } while (input.charCodeAt(idx) !== 41 /*')'*/);
428 idx++;
429 ignored();
430 return vars;
431 }
432}
433
434function fragmentDefinition(): ast.FragmentDefinitionNode {
435 let _name: string | undefined;
436 let _condition: string | undefined;
437 if ((_name = advance(nameRe)) == null) throw error('FragmentDefinition');
438 ignored();
439 if (advance(nameRe) !== 'on') throw error('FragmentDefinition');
440 ignored();
441 if ((_condition = advance(nameRe)) == null) throw error('FragmentDefinition');
442 ignored();
443 const _directives = directives(false);
444 if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('FragmentDefinition');
445 ignored();
446 return {
447 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION,
448 name: { kind: 'Name' as Kind.NAME, value: _name },
449 typeCondition: {
450 kind: 'NamedType' as Kind.NAMED_TYPE,
451 name: { kind: 'Name' as Kind.NAME, value: _condition },
452 },
453 directives: _directives,
454 selectionSet: selectionSet(),
455 };
456}
457
458const definitionRe = /(?:query|mutation|subscription|fragment)/y;
459
460function operationDefinition(
461 operation: OperationTypeNode | undefined
462): ast.OperationDefinitionNode | undefined {
463 let _name: string | undefined;
464 let _variableDefinitions: ast.VariableDefinitionNode[] | undefined;
465 let _directives: ast.DirectiveNode[] | undefined;
466 if (operation) {
467 ignored();
468 _name = advance(nameRe);
469 _variableDefinitions = variableDefinitions();
470 _directives = directives(false);
471 }
472 if (input.charCodeAt(idx) === 123 /*'{'*/) {
473 idx++;
474 ignored();
475 return {
476 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION,
477 operation: operation || ('query' as OperationTypeNode.QUERY),
478 name: _name ? { kind: 'Name' as Kind.NAME, value: _name } : undefined,
479 variableDefinitions: _variableDefinitions,
480 directives: _directives,
481 selectionSet: selectionSet(),
482 };
483 }
484}
485
486function document(): ast.DocumentNode {
487 let match: string | undefined;
488 let definition: ast.OperationDefinitionNode | undefined;
489 ignored();
490 const definitions: ast.ExecutableDefinitionNode[] = [];
491 do {
492 if ((match = advance(definitionRe)) === 'fragment') {
493 ignored();
494 definitions.push(fragmentDefinition());
495 } else if ((definition = operationDefinition(match as OperationTypeNode)) != null) {
496 definitions.push(definition);
497 } else {
498 throw error('Document');
499 }
500 } while (idx < input.length);
501 return {
502 kind: 'Document' as Kind.DOCUMENT,
503 definitions,
504 };
505}
506
507type ParseOptions = {
508 [option: string]: any;
509};
510
511export function parse(
512 string: string | Source,
513 _options?: ParseOptions | undefined
514): ast.DocumentNode {
515 input = typeof string.body === 'string' ? string.body : string;
516 idx = 0;
517 return document();
518}
519
520export function parseValue(
521 string: string | Source,
522 _options?: ParseOptions | undefined
523): ast.ValueNode {
524 input = typeof string.body === 'string' ? string.body : string;
525 idx = 0;
526 ignored();
527 return value(false);
528}
529
530export function parseType(
531 string: string | Source,
532 _options?: ParseOptions | undefined
533): ast.TypeNode {
534 input = typeof string.body === 'string' ? string.body : string;
535 idx = 0;
536 return type();
537}