Mirror: The spec-compliant minimum of client-side GraphQL.

perf: Replace most sticky regexs with pure parser combinators (#52)

Changed files
+319 -270
.changeset
scripts
src
+5
.changeset/red-turkeys-flash.md
···
···
+
---
+
'@0no-co/graphql.web': minor
+
---
+
+
Improve parser performance (up to ~25% higher ops/s) by rewriting part of the parsing that runs in tight loops. Previously, the purer parser combinators w/o regexs wouldn't have been as significant of an improvement, but they now clearly are
-4
scripts/eslint-preset.js
···
message: 'Nullish coalescing assignment (??=) is outside of specified browser support',
},
{
-
selector: 'SequenceExpression',
-
message: 'Sequence expressions are to be avoided since they can be confusing',
-
},
-
{
selector: ':not(ForStatement) > VariableDeclaration[declarations.length>1]',
message: 'Only one variable declarator per variable declaration is preferred',
},
···
message: 'Nullish coalescing assignment (??=) is outside of specified browser support',
},
{
selector: ':not(ForStatement) > VariableDeclaration[declarations.length>1]',
message: 'Only one variable declarator per variable declaration is preferred',
},
-3
scripts/rollup.config.mjs
···
booleans_as_integers: false,
keep_fnames: true,
keep_fargs: true,
-
if_return: false,
ie8: false,
-
sequences: false,
-
loops: false,
conditionals: false,
join_vars: false,
},
···
booleans_as_integers: false,
keep_fnames: true,
keep_fargs: true,
ie8: false,
conditionals: false,
join_vars: false,
},
+37
src/__tests__/parser.test.ts
···
expect(() => {
return parse('{ ...on }');
}).toThrow();
});
it('parses directives on fragment spread', () => {
···
}
`);
}).not.toThrow();
});
it('parses named mutation operations', () => {
···
expect(() => parse('{ ... on Test }')).toThrow();
expect(() => parse('{ ... {} }')).toThrow();
expect(() => parse('{ ... }')).toThrow();
expect(parse('{ ... on Test { field } }')).toHaveProperty(
'definitions.0.selectionSet.selections.0',
···
expect(parseValue({ body: 'null' })).toEqual({ kind: Kind.NULL });
});
it('parses list values', () => {
const result = parseValue('[123 "abc"]');
expect(result).toEqual({
···
kind: Kind.FLOAT,
value: '-1.2e+3',
});
});
it('parses strings', () => {
···
value: ' " ',
block: false,
});
});
it('parses objects', () => {
···
value: ' """ ',
block: true,
});
});
it('allows variables', () => {
···
expect(() => {
return parse('{ ...on }');
}).toThrow();
+
// But does accept "oN"
+
expect(parse('{ ...oN }')).toHaveProperty(
+
'definitions.0.selectionSet.selections.0.name.value',
+
'oN'
+
);
});
it('parses directives on fragment spread', () => {
···
}
`);
}).not.toThrow();
+
});
+
+
it('throws on invalid operations', () => {
+
expect(() => {
+
return parse(`
+
invalid {
+
field
+
}
+
`);
+
}).toThrow();
});
it('parses named mutation operations', () => {
···
expect(() => parse('{ ... on Test }')).toThrow();
expect(() => parse('{ ... {} }')).toThrow();
expect(() => parse('{ ... }')).toThrow();
+
expect(() => parse('{ . }')).toThrow();
expect(parse('{ ... on Test { field } }')).toHaveProperty(
'definitions.0.selectionSet.selections.0',
···
expect(parseValue({ body: 'null' })).toEqual({ kind: Kind.NULL });
});
+
it('parses scalars', () => {
+
expect(parseValue('null')).toEqual({ kind: Kind.NULL });
+
expect(parseValue('true')).toEqual({ kind: Kind.BOOLEAN, value: true });
+
expect(parseValue('false')).toEqual({ kind: Kind.BOOLEAN, value: false });
+
});
+
+
it('parses scalars without optimistic failures', () => {
+
// for *n*ull, *f*alse, *t*rue
+
expect(parseValue('n')).toEqual({ kind: Kind.ENUM, value: 'n' });
+
expect(parseValue('f')).toEqual({ kind: Kind.ENUM, value: 'f' });
+
expect(parseValue('t')).toEqual({ kind: Kind.ENUM, value: 't' });
+
});
+
it('parses list values', () => {
const result = parseValue('[123 "abc"]');
expect(result).toEqual({
···
kind: Kind.FLOAT,
value: '-1.2e+3',
});
+
+
expect(() => parseValue('12e')).toThrow();
});
it('parses strings', () => {
···
value: ' " ',
block: false,
});
+
+
expect(() => parseValue('"')).toThrow();
+
expect(() => parseValue('"\n')).toThrow();
+
expect(() => parseValue('"\r')).toThrow();
});
it('parses objects', () => {
···
value: ' """ ',
block: true,
});
+
+
expect(() => parseValue('"""')).toThrow();
});
it('allows variables', () => {
+277 -263
src/parser.ts
···
idx--;
}
-
const nameRe = /[_A-Za-z]\w*/y;
-
-
// NOTE: This should be compressed by our build step
-
// This merges all possible value parsing into one regular expression
-
const valueRe = new RegExp(
-
'(?:' +
-
// `null`, `true`, and `false` literals (BooleanValue & NullValue)
-
'(null|true|false)|' +
-
// Variables starting with `$` then having a name (VariableNode)
-
'\\$(' +
-
nameRe.source +
-
')|' +
-
// Numbers, starting with int then optionally following with a float part (IntValue and FloatValue)
-
'(-?\\d+)((?:\\.\\d+)?[eE][+-]?\\d+|\\.\\d+)?|' +
-
// Block strings starting with `"""` until the next unescaped `"""` (StringValue)
-
'("""(?:"""|(?:[\\s\\S]*?[^\\\\])"""))|' +
-
// Strings starting with `"` must be on one line (StringValue)
-
'("(?:"|[^\\r\\n]*?[^\\\\]"))|' + // string
-
// Enums are simply names except for our literals (EnumValue)
-
'(' +
-
nameRe.source +
-
'))',
-
'y'
-
);
-
-
// NOTE: Each of the groups above end up in the RegExpExecArray at the specified indices (starting with 1)
-
const enum ValueGroup {
-
Const = 1,
-
Var,
-
Int,
-
Float,
-
BlockString,
-
String,
-
Enum,
}
-
type ValueExec = RegExpExecArray & {
-
[Prop in ValueGroup]: string | undefined;
-
};
-
const complexStringRe = /\\/;
function value(constant: true): ast.ConstValueNode;
function value(constant: boolean): ast.ValueNode;
function value(constant: boolean): ast.ValueNode {
let match: string | undefined;
-
let exec: ValueExec | null;
-
valueRe.lastIndex = idx;
-
if (input.charCodeAt(idx) === 91 /*'['*/) {
-
// Lists are checked ahead of time with `[` chars
-
idx++;
-
ignored();
-
const values: ast.ValueNode[] = [];
-
while (input.charCodeAt(idx) !== 93 /*']'*/) values.push(value(constant));
-
idx++;
-
ignored();
-
return {
-
kind: 'ListValue' as Kind.LIST,
-
values,
-
};
-
} else if (input.charCodeAt(idx) === 123 /*'{'*/) {
-
// Objects are checked ahead of time with `{` chars
-
idx++;
-
ignored();
-
const fields: ast.ObjectFieldNode[] = [];
-
while (input.charCodeAt(idx) !== 125 /*'}'*/) {
-
if ((match = advance(nameRe)) == null) throw error('ObjectField');
ignored();
-
if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField');
ignored();
-
fields.push({
-
kind: 'ObjectField' as Kind.OBJECT_FIELD,
-
name: { kind: 'Name' as Kind.NAME, value: match },
-
value: value(constant),
-
});
-
}
-
idx++;
-
ignored();
-
return {
-
kind: 'ObjectValue' as Kind.OBJECT,
-
fields,
-
};
-
} else if ((exec = valueRe.exec(input) as ValueExec) != null) {
-
// Starting from here, the merged `valueRe` is used
-
idx = valueRe.lastIndex;
-
ignored();
-
if ((match = exec[ValueGroup.Const]) != null) {
-
return match === 'null'
-
? { kind: 'NullValue' as Kind.NULL }
-
: {
-
kind: 'BooleanValue' as Kind.BOOLEAN,
-
value: match === 'true',
-
};
-
} else if ((match = exec[ValueGroup.Var]) != null) {
-
if (constant) {
-
throw error('Variable');
} else {
return {
-
kind: 'Variable' as Kind.VARIABLE,
-
name: {
-
kind: 'Name' as Kind.NAME,
-
value: match,
-
},
};
}
-
} else if ((match = exec[ValueGroup.Int]) != null) {
-
let floatPart: string | undefined;
-
if ((floatPart = exec[ValueGroup.Float]) != null) {
return {
kind: 'FloatValue' as Kind.FLOAT,
-
value: match + floatPart,
};
} else {
return {
kind: 'IntValue' as Kind.INT,
-
value: match,
};
}
-
} else if ((match = exec[ValueGroup.BlockString]) != null) {
-
return {
-
kind: 'StringValue' as Kind.STRING,
-
value: blockString(match.slice(3, -3)),
-
block: true,
-
};
-
} else if ((match = exec[ValueGroup.String]) != null) {
-
return {
-
kind: 'StringValue' as Kind.STRING,
-
// When strings don't contain escape codes, a simple slice will be enough, otherwise
-
// `JSON.parse` matches GraphQL's string parsing perfectly
-
value: complexStringRe.test(match) ? (JSON.parse(match) as string) : match.slice(1, -1),
-
block: false,
-
};
-
} else if ((match = exec[ValueGroup.Enum]) != null) {
-
return {
-
kind: 'EnumValue' as Kind.ENUM,
-
value: match,
-
};
-
}
}
-
throw error('Value');
}
function arguments_(constant: boolean): ast.ArgumentNode[] | undefined {
···
const args: ast.ArgumentNode[] = [];
idx++;
ignored();
-
let _name: string | undefined;
do {
-
if ((_name = advance(nameRe)) == null) throw error('Argument');
-
ignored();
if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument');
ignored();
args.push({
kind: 'Argument' as Kind.ARGUMENT,
-
name: { kind: 'Name' as Kind.NAME, value: _name },
value: value(constant),
});
} while (input.charCodeAt(idx) !== 41 /*')'*/);
···
function directives(constant: boolean): ast.DirectiveNode[] | undefined {
if (input.charCodeAt(idx) === 64 /*'@'*/) {
const directives: ast.DirectiveNode[] = [];
-
let _name: string | undefined;
do {
idx++;
-
if ((_name = advance(nameRe)) == null) throw error('Directive');
-
ignored();
directives.push({
kind: 'Directive' as Kind.DIRECTIVE,
-
name: { kind: 'Name' as Kind.NAME, value: _name },
arguments: arguments_(constant),
});
} while (input.charCodeAt(idx) === 64 /*'@'*/);
···
}
function type(): ast.TypeNode {
-
let match: string | undefined;
let lists = 0;
while (input.charCodeAt(idx) === 91 /*'['*/) {
lists++;
idx++;
ignored();
}
-
if ((match = advance(nameRe)) == null) throw error('NamedType');
-
ignored();
let type: ast.TypeNode = {
kind: 'NamedType' as Kind.NAMED_TYPE,
-
name: { kind: 'Name' as Kind.NAME, value: match },
};
do {
if (input.charCodeAt(idx) === 33 /*'!'*/) {
···
return type;
}
-
// NOTE: This should be compressed by our build step
-
// This merges the two possible selection parsing branches into one regular expression
-
const selectionRe = new RegExp(
-
'(?:' +
-
// fragment spreads (FragmentSpread or InlineFragment nodes)
-
'(\\.{3})|' +
-
// field aliases or names (FieldNode)
-
'(' +
-
nameRe.source +
-
'))',
-
'y'
-
);
-
-
// NOTE: Each of the groups above end up in the RegExpExecArray at the indices 1&2
-
const enum SelectionGroup {
-
Spread = 1,
-
Name,
}
-
type SelectionExec = RegExpExecArray & {
-
[Prop in SelectionGroup]: string | undefined;
-
};
-
function selectionSet(): ast.SelectionSetNode {
const selections: ast.SelectionNode[] = [];
-
let match: string | undefined;
-
let exec: SelectionExec | null;
do {
-
selectionRe.lastIndex = idx;
-
if ((exec = selectionRe.exec(input) as SelectionExec) != null) {
-
idx = selectionRe.lastIndex;
-
if (exec[SelectionGroup.Spread] != null) {
-
ignored();
-
let match = advance(nameRe);
-
if (match != null && match !== 'on') {
-
// A simple `...Name` spread with optional directives
-
ignored();
selections.push({
-
kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
-
name: { kind: 'Name' as Kind.NAME, value: match },
directives: directives(false),
});
-
} else {
-
ignored();
-
if (match === 'on') {
-
// An inline `... on Name` spread; if this doesn't match, the type condition has been omitted
-
if ((match = advance(nameRe)) == null) throw error('NamedType');
ignored();
}
-
const _directives = directives(false);
-
if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('InlineFragment');
ignored();
selections.push({
kind: 'InlineFragment' as Kind.INLINE_FRAGMENT,
-
typeCondition: match
-
? {
-
kind: 'NamedType' as Kind.NAMED_TYPE,
-
name: { kind: 'Name' as Kind.NAME, value: match },
-
}
-
: undefined,
-
directives: _directives,
selectionSet: selectionSet(),
});
-
}
-
} else if ((match = exec[SelectionGroup.Name]) != null) {
-
let _alias: string | undefined;
ignored();
-
// Parse the optional alias, by reassigning and then getting the name
-
if (input.charCodeAt(idx) === 58 /*':'*/) {
-
idx++;
-
ignored();
-
_alias = match;
-
if ((match = advance(nameRe)) == null) throw error('Field');
-
ignored();
-
}
-
const _arguments = arguments_(false);
ignored();
-
const _directives = directives(false);
-
let _selectionSet: ast.SelectionSetNode | undefined;
-
if (input.charCodeAt(idx) === 123 /*'{'*/) {
-
idx++;
-
ignored();
-
_selectionSet = selectionSet();
-
}
-
selections.push({
-
kind: 'Field' as Kind.FIELD,
-
alias: _alias ? { kind: 'Name' as Kind.NAME, value: _alias } : undefined,
-
name: { kind: 'Name' as Kind.NAME, value: match },
-
arguments: _arguments,
-
directives: _directives,
-
selectionSet: _selectionSet,
-
});
}
-
} else {
-
throw error('SelectionSet');
}
} while (input.charCodeAt(idx) !== 125 /*'}'*/);
idx++;
···
const vars: ast.VariableDefinitionNode[] = [];
idx++;
ignored();
-
let _name: string | undefined;
do {
if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable');
-
if ((_name = advance(nameRe)) == null) throw error('Variable');
-
ignored();
if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition');
ignored();
const _type = type();
···
kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION,
variable: {
kind: 'Variable' as Kind.VARIABLE,
-
name: { kind: 'Name' as Kind.NAME, value: _name },
},
type: _type,
defaultValue: _defaultValue,
···
}
function fragmentDefinition(): ast.FragmentDefinitionNode {
-
let _name: string | undefined;
-
let _condition: string | undefined;
-
if ((_name = advance(nameRe)) == null) throw error('FragmentDefinition');
-
ignored();
-
if (advance(nameRe) !== 'on') throw error('FragmentDefinition');
-
ignored();
-
if ((_condition = advance(nameRe)) == null) throw error('FragmentDefinition');
-
ignored();
-
const _directives = directives(false);
-
if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('FragmentDefinition');
ignored();
return {
kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION,
-
name: { kind: 'Name' as Kind.NAME, value: _name },
typeCondition: {
kind: 'NamedType' as Kind.NAMED_TYPE,
-
name: { kind: 'Name' as Kind.NAME, value: _condition },
},
-
directives: _directives,
-
selectionSet: selectionSet(),
};
}
-
const definitionRe = /(?:query|mutation|subscription|fragment)/y;
-
-
function operationDefinition(
-
operation: OperationTypeNode | undefined
-
): ast.OperationDefinitionNode | undefined {
-
let _name: string | undefined;
-
let _variableDefinitions: ast.VariableDefinitionNode[] | undefined;
-
let _directives: ast.DirectiveNode[] | undefined;
-
if (operation) {
-
ignored();
-
_name = advance(nameRe);
-
_variableDefinitions = variableDefinitions();
-
_directives = directives(false);
-
}
-
if (input.charCodeAt(idx) === 123 /*'{'*/) {
-
idx++;
-
ignored();
-
return {
-
kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION,
-
operation: operation || ('query' as OperationTypeNode.QUERY),
-
name: _name ? { kind: 'Name' as Kind.NAME, value: _name } : undefined,
-
variableDefinitions: _variableDefinitions,
-
directives: _directives,
-
selectionSet: selectionSet(),
-
};
-
}
-
}
-
function document(input: string, noLoc: boolean): ast.DocumentNode {
-
let match: string | undefined;
-
let definition: ast.OperationDefinitionNode | undefined;
ignored();
const definitions: ast.ExecutableDefinitionNode[] = [];
do {
-
if ((match = advance(definitionRe)) === 'fragment') {
-
ignored();
-
definitions.push(fragmentDefinition());
-
} else if ((definition = operationDefinition(match as OperationTypeNode)) != null) {
-
definitions.push(definition);
} else {
-
throw error('Document');
}
} while (idx < input.length);
···
string: string | Source,
options?: ParseOptions | undefined
): ast.DocumentNode {
-
input = typeof string.body === 'string' ? string.body : string;
idx = 0;
return document(input, options && options.noLocation);
}
···
string: string | Source,
_options?: ParseOptions | undefined
): ast.ValueNode {
-
input = typeof string.body === 'string' ? string.body : string;
idx = 0;
ignored();
return value(false);
···
string: string | Source,
_options?: ParseOptions | undefined
): ast.TypeNode {
-
input = typeof string.body === 'string' ? string.body : string;
idx = 0;
return type();
}
···
idx--;
}
+
function name(): string {
+
const start = idx;
+
for (
+
let char = input.charCodeAt(idx++) | 0;
+
(char >= 48 /*'0'*/ && char <= 57) /*'9'*/ ||
+
(char >= 65 /*'A'*/ && char <= 90) /*'Z'*/ ||
+
char === 95 /*'_'*/ ||
+
(char >= 97 /*'a'*/ && char <= 122) /*'z'*/;
+
char = input.charCodeAt(idx++) | 0
+
);
+
if (start === idx - 1) throw error('Name');
+
const value = input.slice(start, --idx);
+
ignored();
+
return value;
}
+
function nameNode(): ast.NameNode {
+
return {
+
kind: 'Name' as Kind.NAME,
+
value: name(),
+
};
+
}
+
const restBlockStringRe = /(?:"""|(?:[\s\S]*?[^\\])""")/y;
+
const floatPartRe = /(?:(?:\.\d+)?[eE][+-]?\d+|\.\d+)/y;
function value(constant: true): ast.ConstValueNode;
function value(constant: boolean): ast.ValueNode;
function value(constant: boolean): ast.ValueNode {
let match: string | undefined;
+
switch (input.charCodeAt(idx)) {
+
case 91: // '['
+
idx++;
+
ignored();
+
const values: ast.ValueNode[] = [];
+
while (input.charCodeAt(idx) !== 93 /*']'*/) values.push(value(constant));
+
idx++;
+
ignored();
+
return {
+
kind: 'ListValue' as Kind.LIST,
+
values,
+
};
+
+
case 123: // '{'
+
idx++;
ignored();
+
const fields: ast.ObjectFieldNode[] = [];
+
while (input.charCodeAt(idx) !== 125 /*'}'*/) {
+
const name = nameNode();
+
if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField');
+
ignored();
+
fields.push({
+
kind: 'ObjectField' as Kind.OBJECT_FIELD,
+
name,
+
value: value(constant),
+
});
+
}
+
idx++;
ignored();
+
return {
+
kind: 'ObjectValue' as Kind.OBJECT,
+
fields,
+
};
+
+
case 36: // '$'
+
if (constant) throw error('Variable');
+
idx++;
+
return {
+
kind: 'Variable' as Kind.VARIABLE,
+
name: nameNode(),
+
};
+
+
case 34: // '"'
+
if (input.charCodeAt(idx + 1) === 34 && input.charCodeAt(idx + 2) === 34) {
+
idx += 3;
+
if ((match = advance(restBlockStringRe)) == null) throw error('StringValue');
+
ignored();
+
return {
+
kind: 'StringValue' as Kind.STRING,
+
value: blockString(match.slice(0, -3)),
+
block: true,
+
};
} else {
+
const start = idx;
+
idx++;
+
let char: number;
+
let isComplex = false;
+
for (
+
char = input.charCodeAt(idx++) | 0;
+
(char === 92 /*'\\'*/ && (idx++, (isComplex = true))) ||
+
(char !== 10 /*'\n'*/ && char !== 13 /*'\r'*/ && char !== 34 /*'"'*/ && char);
+
char = input.charCodeAt(idx++) | 0
+
);
+
if (char !== 34) throw error('StringValue');
+
match = input.slice(start, idx);
+
ignored();
return {
+
kind: 'StringValue' as Kind.STRING,
+
value: isComplex ? (JSON.parse(match) as string) : match.slice(1, -1),
+
block: false,
};
}
+
+
case 45: // '-'
+
case 48: // '0'
+
case 49: // '1'
+
case 50: // '2'
+
case 51: // '3'
+
case 52: // '4'
+
case 53: // '5'
+
case 54: // '6'
+
case 55: // '7'
+
case 56: // '8'
+
case 57: // '9'
+
const start = idx++;
+
let char: number;
+
while ((char = input.charCodeAt(idx++) | 0) >= 48 /*'0'*/ && char <= 57 /*'9'*/);
+
const intPart = input.slice(start, --idx);
+
if (
+
(char = input.charCodeAt(idx)) === 46 /*'.'*/ ||
+
char === 69 /*'E'*/ ||
+
char === 101 /*'e'*/
+
) {
+
if ((match = advance(floatPartRe)) == null) throw error('FloatValue');
+
ignored();
return {
kind: 'FloatValue' as Kind.FLOAT,
+
value: intPart + match,
};
} else {
+
ignored();
return {
kind: 'IntValue' as Kind.INT,
+
value: intPart,
};
}
+
+
case 110: // 'n'
+
if (
+
input.charCodeAt(idx + 1) === 117 &&
+
input.charCodeAt(idx + 2) === 108 &&
+
input.charCodeAt(idx + 3) === 108
+
) {
+
idx += 4;
+
ignored();
+
return { kind: 'NullValue' as Kind.NULL };
+
} else break;
+
+
case 116: // 't'
+
if (
+
input.charCodeAt(idx + 1) === 114 &&
+
input.charCodeAt(idx + 2) === 117 &&
+
input.charCodeAt(idx + 3) === 101
+
) {
+
idx += 4;
+
ignored();
+
return { kind: 'BooleanValue' as Kind.BOOLEAN, value: true };
+
} else break;
+
+
case 102: // 'f'
+
if (
+
input.charCodeAt(idx + 1) === 97 &&
+
input.charCodeAt(idx + 2) === 108 &&
+
input.charCodeAt(idx + 3) === 115 &&
+
input.charCodeAt(idx + 4) === 101
+
) {
+
idx += 5;
+
ignored();
+
return { kind: 'BooleanValue' as Kind.BOOLEAN, value: false };
+
} else break;
}
+
return {
+
kind: 'EnumValue' as Kind.ENUM,
+
value: name(),
+
};
}
function arguments_(constant: boolean): ast.ArgumentNode[] | undefined {
···
const args: ast.ArgumentNode[] = [];
idx++;
ignored();
do {
+
const name = nameNode();
if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument');
ignored();
args.push({
kind: 'Argument' as Kind.ARGUMENT,
+
name,
value: value(constant),
});
} while (input.charCodeAt(idx) !== 41 /*')'*/);
···
function directives(constant: boolean): ast.DirectiveNode[] | undefined {
if (input.charCodeAt(idx) === 64 /*'@'*/) {
const directives: ast.DirectiveNode[] = [];
do {
idx++;
directives.push({
kind: 'Directive' as Kind.DIRECTIVE,
+
name: nameNode(),
arguments: arguments_(constant),
});
} while (input.charCodeAt(idx) === 64 /*'@'*/);
···
}
function type(): ast.TypeNode {
let lists = 0;
while (input.charCodeAt(idx) === 91 /*'['*/) {
lists++;
idx++;
ignored();
}
let type: ast.TypeNode = {
kind: 'NamedType' as Kind.NAMED_TYPE,
+
name: nameNode(),
};
do {
if (input.charCodeAt(idx) === 33 /*'!'*/) {
···
return type;
}
+
function selectionSetStart(): ast.SelectionSetNode {
+
if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('SelectionSet');
+
ignored();
+
return selectionSet();
}
function selectionSet(): ast.SelectionSetNode {
const selections: ast.SelectionNode[] = [];
do {
+
if (input.charCodeAt(idx) === 46 /*'.'*/) {
+
if (input.charCodeAt(++idx) !== 46 /*'.'*/ || input.charCodeAt(++idx) !== 46 /*'.'*/)
+
throw error('SelectionSet');
+
idx++;
+
ignored();
+
switch (input.charCodeAt(idx)) {
+
case 64 /*'@'*/:
selections.push({
+
kind: 'InlineFragment' as Kind.INLINE_FRAGMENT,
+
typeCondition: undefined,
directives: directives(false),
+
selectionSet: selectionSetStart(),
});
+
break;
+
+
case 111 /*'o'*/:
+
if (input.charCodeAt(idx + 1) === 110 /*'n'*/) {
+
idx += 2;
ignored();
+
selections.push({
+
kind: 'InlineFragment' as Kind.INLINE_FRAGMENT,
+
typeCondition: {
+
kind: 'NamedType' as Kind.NAMED_TYPE,
+
name: nameNode(),
+
},
+
directives: directives(false),
+
selectionSet: selectionSetStart(),
+
});
+
} else {
+
selections.push({
+
kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
+
name: nameNode(),
+
directives: directives(false),
+
});
}
+
break;
+
+
case 123 /*'{'*/:
+
idx++;
ignored();
selections.push({
kind: 'InlineFragment' as Kind.INLINE_FRAGMENT,
+
typeCondition: undefined,
+
directives: undefined,
selectionSet: selectionSet(),
});
+
break;
+
+
default:
+
selections.push({
+
kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
+
name: nameNode(),
+
directives: directives(false),
+
});
+
}
+
} else {
+
let name = nameNode();
+
let alias: ast.NameNode | undefined;
+
if (input.charCodeAt(idx) === 58 /*':'*/) {
+
idx++;
ignored();
+
alias = name;
+
name = nameNode();
+
}
+
const _arguments = arguments_(false);
+
const _directives = directives(false);
+
let _selectionSet: ast.SelectionSetNode | undefined;
+
if (input.charCodeAt(idx) === 123 /*'{'*/) {
+
idx++;
ignored();
+
_selectionSet = selectionSet();
}
+
selections.push({
+
kind: 'Field' as Kind.FIELD,
+
alias,
+
name,
+
arguments: _arguments,
+
directives: _directives,
+
selectionSet: _selectionSet,
+
});
}
} while (input.charCodeAt(idx) !== 125 /*'}'*/);
idx++;
···
const vars: ast.VariableDefinitionNode[] = [];
idx++;
ignored();
do {
if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable');
+
const name = nameNode();
if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition');
ignored();
const _type = type();
···
kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION,
variable: {
kind: 'Variable' as Kind.VARIABLE,
+
name,
},
type: _type,
defaultValue: _defaultValue,
···
}
function fragmentDefinition(): ast.FragmentDefinitionNode {
+
const name = nameNode();
+
if (input.charCodeAt(idx++) !== 111 /*'o'*/ || input.charCodeAt(idx++) !== 110 /*'n'*/)
+
throw error('FragmentDefinition');
ignored();
return {
kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION,
+
name,
typeCondition: {
kind: 'NamedType' as Kind.NAMED_TYPE,
+
name: nameNode(),
},
+
directives: directives(false),
+
selectionSet: selectionSetStart(),
};
}
function document(input: string, noLoc: boolean): ast.DocumentNode {
ignored();
const definitions: ast.ExecutableDefinitionNode[] = [];
do {
+
if (input.charCodeAt(idx) === 123 /*'{'*/) {
+
definitions.push({
+
kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION,
+
operation: 'query' as OperationTypeNode.QUERY,
+
name: undefined,
+
variableDefinitions: undefined,
+
directives: undefined,
+
selectionSet: selectionSetStart(),
+
});
} else {
+
const definition = name();
+
switch (definition) {
+
case 'fragment':
+
definitions.push(fragmentDefinition());
+
break;
+
case 'query':
+
case 'mutation':
+
case 'subscription':
+
let char: number;
+
let name: ast.NameNode | undefined;
+
if (
+
(char = input.charCodeAt(idx)) !== 40 /*'('*/ &&
+
char !== 64 /*'@'*/ &&
+
char !== 123 /*'{'*/
+
) {
+
name = nameNode();
+
}
+
definitions.push({
+
kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION,
+
operation: definition as OperationTypeNode,
+
name,
+
variableDefinitions: variableDefinitions(),
+
directives: directives(false),
+
selectionSet: selectionSetStart(),
+
});
+
break;
+
default:
+
throw error('Document');
+
}
}
} while (idx < input.length);
···
string: string | Source,
options?: ParseOptions | undefined
): ast.DocumentNode {
+
input = string.body ? string.body : string;
idx = 0;
return document(input, options && options.noLocation);
}
···
string: string | Source,
_options?: ParseOptions | undefined
): ast.ValueNode {
+
input = string.body ? string.body : string;
idx = 0;
ignored();
return value(false);
···
string: string | Source,
_options?: ParseOptions | undefined
): ast.TypeNode {
+
input = string.body ? string.body : string;
idx = 0;
return type();
}