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

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

authored by kitten.sh and committed by

GitHub 2984e7be 5c7a5a5b

+319 -270
+5
.changeset/red-turkeys-flash.md
··· 1 + --- 2 + '@0no-co/graphql.web': minor 3 + --- 4 + 5 + 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
··· 70 70 message: 'Nullish coalescing assignment (??=) is outside of specified browser support', 71 71 }, 72 72 { 73 - selector: 'SequenceExpression', 74 - message: 'Sequence expressions are to be avoided since they can be confusing', 75 - }, 76 - { 77 73 selector: ':not(ForStatement) > VariableDeclaration[declarations.length>1]', 78 74 message: 'Only one variable declarator per variable declaration is preferred', 79 75 },
-3
scripts/rollup.config.mjs
··· 142 142 booleans_as_integers: false, 143 143 keep_fnames: true, 144 144 keep_fargs: true, 145 - if_return: false, 146 145 ie8: false, 147 - sequences: false, 148 - loops: false, 149 146 conditionals: false, 150 147 join_vars: false, 151 148 },
+37
src/__tests__/parser.test.ts
··· 42 42 expect(() => { 43 43 return parse('{ ...on }'); 44 44 }).toThrow(); 45 + // But does accept "oN" 46 + expect(parse('{ ...oN }')).toHaveProperty( 47 + 'definitions.0.selectionSet.selections.0.name.value', 48 + 'oN' 49 + ); 45 50 }); 46 51 47 52 it('parses directives on fragment spread', () => { ··· 121 126 } 122 127 `); 123 128 }).not.toThrow(); 129 + }); 130 + 131 + it('throws on invalid operations', () => { 132 + expect(() => { 133 + return parse(` 134 + invalid { 135 + field 136 + } 137 + `); 138 + }).toThrow(); 124 139 }); 125 140 126 141 it('parses named mutation operations', () => { ··· 255 270 expect(() => parse('{ ... on Test }')).toThrow(); 256 271 expect(() => parse('{ ... {} }')).toThrow(); 257 272 expect(() => parse('{ ... }')).toThrow(); 273 + expect(() => parse('{ . }')).toThrow(); 258 274 259 275 expect(parse('{ ... on Test { field } }')).toHaveProperty( 260 276 'definitions.0.selectionSet.selections.0', ··· 497 513 expect(parseValue({ body: 'null' })).toEqual({ kind: Kind.NULL }); 498 514 }); 499 515 516 + it('parses scalars', () => { 517 + expect(parseValue('null')).toEqual({ kind: Kind.NULL }); 518 + expect(parseValue('true')).toEqual({ kind: Kind.BOOLEAN, value: true }); 519 + expect(parseValue('false')).toEqual({ kind: Kind.BOOLEAN, value: false }); 520 + }); 521 + 522 + it('parses scalars without optimistic failures', () => { 523 + // for *n*ull, *f*alse, *t*rue 524 + expect(parseValue('n')).toEqual({ kind: Kind.ENUM, value: 'n' }); 525 + expect(parseValue('f')).toEqual({ kind: Kind.ENUM, value: 'f' }); 526 + expect(parseValue('t')).toEqual({ kind: Kind.ENUM, value: 't' }); 527 + }); 528 + 500 529 it('parses list values', () => { 501 530 const result = parseValue('[123 "abc"]'); 502 531 expect(result).toEqual({ ··· 542 571 kind: Kind.FLOAT, 543 572 value: '-1.2e+3', 544 573 }); 574 + 575 + expect(() => parseValue('12e')).toThrow(); 545 576 }); 546 577 547 578 it('parses strings', () => { ··· 580 611 value: ' " ', 581 612 block: false, 582 613 }); 614 + 615 + expect(() => parseValue('"')).toThrow(); 616 + expect(() => parseValue('"\n')).toThrow(); 617 + expect(() => parseValue('"\r')).toThrow(); 583 618 }); 584 619 585 620 it('parses objects', () => { ··· 674 709 value: ' """ ', 675 710 block: true, 676 711 }); 712 + 713 + expect(() => parseValue('"""')).toThrow(); 677 714 }); 678 715 679 716 it('allows variables', () => {
+277 -263
src/parser.ts
··· 65 65 idx--; 66 66 } 67 67 68 - const 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 72 - const 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) 94 - const enum ValueGroup { 95 - Const = 1, 96 - Var, 97 - Int, 98 - Float, 99 - BlockString, 100 - String, 101 - Enum, 68 + function name(): string { 69 + const start = idx; 70 + for ( 71 + let char = input.charCodeAt(idx++) | 0; 72 + (char >= 48 /*'0'*/ && char <= 57) /*'9'*/ || 73 + (char >= 65 /*'A'*/ && char <= 90) /*'Z'*/ || 74 + char === 95 /*'_'*/ || 75 + (char >= 97 /*'a'*/ && char <= 122) /*'z'*/; 76 + char = input.charCodeAt(idx++) | 0 77 + ); 78 + if (start === idx - 1) throw error('Name'); 79 + const value = input.slice(start, --idx); 80 + ignored(); 81 + return value; 102 82 } 103 83 104 - type ValueExec = RegExpExecArray & { 105 - [Prop in ValueGroup]: string | undefined; 106 - }; 84 + function nameNode(): ast.NameNode { 85 + return { 86 + kind: 'Name' as Kind.NAME, 87 + value: name(), 88 + }; 89 + } 107 90 108 - const complexStringRe = /\\/; 91 + const restBlockStringRe = /(?:"""|(?:[\s\S]*?[^\\])""")/y; 92 + const floatPartRe = /(?:(?:\.\d+)?[eE][+-]?\d+|\.\d+)/y; 109 93 110 94 function value(constant: true): ast.ConstValueNode; 111 95 function value(constant: boolean): ast.ValueNode; 112 96 113 97 function value(constant: boolean): ast.ValueNode { 114 98 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'); 99 + switch (input.charCodeAt(idx)) { 100 + case 91: // '[' 101 + idx++; 102 + ignored(); 103 + const values: ast.ValueNode[] = []; 104 + while (input.charCodeAt(idx) !== 93 /*']'*/) values.push(value(constant)); 105 + idx++; 106 + ignored(); 107 + return { 108 + kind: 'ListValue' as Kind.LIST, 109 + values, 110 + }; 111 + 112 + case 123: // '{' 113 + idx++; 136 114 ignored(); 137 - if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField'); 115 + const fields: ast.ObjectFieldNode[] = []; 116 + while (input.charCodeAt(idx) !== 125 /*'}'*/) { 117 + const name = nameNode(); 118 + if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField'); 119 + ignored(); 120 + fields.push({ 121 + kind: 'ObjectField' as Kind.OBJECT_FIELD, 122 + name, 123 + value: value(constant), 124 + }); 125 + } 126 + idx++; 138 127 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'); 128 + return { 129 + kind: 'ObjectValue' as Kind.OBJECT, 130 + fields, 131 + }; 132 + 133 + case 36: // '$' 134 + if (constant) throw error('Variable'); 135 + idx++; 136 + return { 137 + kind: 'Variable' as Kind.VARIABLE, 138 + name: nameNode(), 139 + }; 140 + 141 + case 34: // '"' 142 + if (input.charCodeAt(idx + 1) === 34 && input.charCodeAt(idx + 2) === 34) { 143 + idx += 3; 144 + if ((match = advance(restBlockStringRe)) == null) throw error('StringValue'); 145 + ignored(); 146 + return { 147 + kind: 'StringValue' as Kind.STRING, 148 + value: blockString(match.slice(0, -3)), 149 + block: true, 150 + }; 165 151 } else { 152 + const start = idx; 153 + idx++; 154 + let char: number; 155 + let isComplex = false; 156 + for ( 157 + char = input.charCodeAt(idx++) | 0; 158 + (char === 92 /*'\\'*/ && (idx++, (isComplex = true))) || 159 + (char !== 10 /*'\n'*/ && char !== 13 /*'\r'*/ && char !== 34 /*'"'*/ && char); 160 + char = input.charCodeAt(idx++) | 0 161 + ); 162 + if (char !== 34) throw error('StringValue'); 163 + match = input.slice(start, idx); 164 + ignored(); 166 165 return { 167 - kind: 'Variable' as Kind.VARIABLE, 168 - name: { 169 - kind: 'Name' as Kind.NAME, 170 - value: match, 171 - }, 166 + kind: 'StringValue' as Kind.STRING, 167 + value: isComplex ? (JSON.parse(match) as string) : match.slice(1, -1), 168 + block: false, 172 169 }; 173 170 } 174 - } else if ((match = exec[ValueGroup.Int]) != null) { 175 - let floatPart: string | undefined; 176 - if ((floatPart = exec[ValueGroup.Float]) != null) { 171 + 172 + case 45: // '-' 173 + case 48: // '0' 174 + case 49: // '1' 175 + case 50: // '2' 176 + case 51: // '3' 177 + case 52: // '4' 178 + case 53: // '5' 179 + case 54: // '6' 180 + case 55: // '7' 181 + case 56: // '8' 182 + case 57: // '9' 183 + const start = idx++; 184 + let char: number; 185 + while ((char = input.charCodeAt(idx++) | 0) >= 48 /*'0'*/ && char <= 57 /*'9'*/); 186 + const intPart = input.slice(start, --idx); 187 + if ( 188 + (char = input.charCodeAt(idx)) === 46 /*'.'*/ || 189 + char === 69 /*'E'*/ || 190 + char === 101 /*'e'*/ 191 + ) { 192 + if ((match = advance(floatPartRe)) == null) throw error('FloatValue'); 193 + ignored(); 177 194 return { 178 195 kind: 'FloatValue' as Kind.FLOAT, 179 - value: match + floatPart, 196 + value: intPart + match, 180 197 }; 181 198 } else { 199 + ignored(); 182 200 return { 183 201 kind: 'IntValue' as Kind.INT, 184 - value: match, 202 + value: intPart, 185 203 }; 186 204 } 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 - } 205 + 206 + case 110: // 'n' 207 + if ( 208 + input.charCodeAt(idx + 1) === 117 && 209 + input.charCodeAt(idx + 2) === 108 && 210 + input.charCodeAt(idx + 3) === 108 211 + ) { 212 + idx += 4; 213 + ignored(); 214 + return { kind: 'NullValue' as Kind.NULL }; 215 + } else break; 216 + 217 + case 116: // 't' 218 + if ( 219 + input.charCodeAt(idx + 1) === 114 && 220 + input.charCodeAt(idx + 2) === 117 && 221 + input.charCodeAt(idx + 3) === 101 222 + ) { 223 + idx += 4; 224 + ignored(); 225 + return { kind: 'BooleanValue' as Kind.BOOLEAN, value: true }; 226 + } else break; 227 + 228 + case 102: // 'f' 229 + if ( 230 + input.charCodeAt(idx + 1) === 97 && 231 + input.charCodeAt(idx + 2) === 108 && 232 + input.charCodeAt(idx + 3) === 115 && 233 + input.charCodeAt(idx + 4) === 101 234 + ) { 235 + idx += 5; 236 + ignored(); 237 + return { kind: 'BooleanValue' as Kind.BOOLEAN, value: false }; 238 + } else break; 207 239 } 208 240 209 - throw error('Value'); 241 + return { 242 + kind: 'EnumValue' as Kind.ENUM, 243 + value: name(), 244 + }; 210 245 } 211 246 212 247 function arguments_(constant: boolean): ast.ArgumentNode[] | undefined { ··· 214 249 const args: ast.ArgumentNode[] = []; 215 250 idx++; 216 251 ignored(); 217 - let _name: string | undefined; 218 252 do { 219 - if ((_name = advance(nameRe)) == null) throw error('Argument'); 220 - ignored(); 253 + const name = nameNode(); 221 254 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument'); 222 255 ignored(); 223 256 args.push({ 224 257 kind: 'Argument' as Kind.ARGUMENT, 225 - name: { kind: 'Name' as Kind.NAME, value: _name }, 258 + name, 226 259 value: value(constant), 227 260 }); 228 261 } while (input.charCodeAt(idx) !== 41 /*')'*/); ··· 238 271 function directives(constant: boolean): ast.DirectiveNode[] | undefined { 239 272 if (input.charCodeAt(idx) === 64 /*'@'*/) { 240 273 const directives: ast.DirectiveNode[] = []; 241 - let _name: string | undefined; 242 274 do { 243 275 idx++; 244 - if ((_name = advance(nameRe)) == null) throw error('Directive'); 245 - ignored(); 246 276 directives.push({ 247 277 kind: 'Directive' as Kind.DIRECTIVE, 248 - name: { kind: 'Name' as Kind.NAME, value: _name }, 278 + name: nameNode(), 249 279 arguments: arguments_(constant), 250 280 }); 251 281 } while (input.charCodeAt(idx) === 64 /*'@'*/); ··· 254 284 } 255 285 256 286 function type(): ast.TypeNode { 257 - let match: string | undefined; 258 287 let lists = 0; 259 288 while (input.charCodeAt(idx) === 91 /*'['*/) { 260 289 lists++; 261 290 idx++; 262 291 ignored(); 263 292 } 264 - if ((match = advance(nameRe)) == null) throw error('NamedType'); 265 - ignored(); 266 293 let type: ast.TypeNode = { 267 294 kind: 'NamedType' as Kind.NAMED_TYPE, 268 - name: { kind: 'Name' as Kind.NAME, value: match }, 295 + name: nameNode(), 269 296 }; 270 297 do { 271 298 if (input.charCodeAt(idx) === 33 /*'!'*/) { ··· 288 315 return type; 289 316 } 290 317 291 - // NOTE: This should be compressed by our build step 292 - // This merges the two possible selection parsing branches into one regular expression 293 - const 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 305 - const enum SelectionGroup { 306 - Spread = 1, 307 - Name, 318 + function selectionSetStart(): ast.SelectionSetNode { 319 + if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('SelectionSet'); 320 + ignored(); 321 + return selectionSet(); 308 322 } 309 323 310 - type SelectionExec = RegExpExecArray & { 311 - [Prop in SelectionGroup]: string | undefined; 312 - }; 313 - 314 324 function selectionSet(): ast.SelectionSetNode { 315 325 const selections: ast.SelectionNode[] = []; 316 - let match: string | undefined; 317 - let exec: SelectionExec | null; 318 326 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(); 327 + if (input.charCodeAt(idx) === 46 /*'.'*/) { 328 + if (input.charCodeAt(++idx) !== 46 /*'.'*/ || input.charCodeAt(++idx) !== 46 /*'.'*/) 329 + throw error('SelectionSet'); 330 + idx++; 331 + ignored(); 332 + switch (input.charCodeAt(idx)) { 333 + case 64 /*'@'*/: 328 334 selections.push({ 329 - kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, 330 - name: { kind: 'Name' as Kind.NAME, value: match }, 335 + kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, 336 + typeCondition: undefined, 331 337 directives: directives(false), 338 + selectionSet: selectionSetStart(), 332 339 }); 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'); 340 + break; 341 + 342 + case 111 /*'o'*/: 343 + if (input.charCodeAt(idx + 1) === 110 /*'n'*/) { 344 + idx += 2; 338 345 ignored(); 346 + selections.push({ 347 + kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, 348 + typeCondition: { 349 + kind: 'NamedType' as Kind.NAMED_TYPE, 350 + name: nameNode(), 351 + }, 352 + directives: directives(false), 353 + selectionSet: selectionSetStart(), 354 + }); 355 + } else { 356 + selections.push({ 357 + kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, 358 + name: nameNode(), 359 + directives: directives(false), 360 + }); 339 361 } 340 - const _directives = directives(false); 341 - if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('InlineFragment'); 362 + break; 363 + 364 + case 123 /*'{'*/: 365 + idx++; 342 366 ignored(); 343 367 selections.push({ 344 368 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, 369 + typeCondition: undefined, 370 + directives: undefined, 352 371 selectionSet: selectionSet(), 353 372 }); 354 - } 355 - } else if ((match = exec[SelectionGroup.Name]) != null) { 356 - let _alias: string | undefined; 373 + break; 374 + 375 + default: 376 + selections.push({ 377 + kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, 378 + name: nameNode(), 379 + directives: directives(false), 380 + }); 381 + } 382 + } else { 383 + let name = nameNode(); 384 + let alias: ast.NameNode | undefined; 385 + if (input.charCodeAt(idx) === 58 /*':'*/) { 386 + idx++; 357 387 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); 388 + alias = name; 389 + name = nameNode(); 390 + } 391 + const _arguments = arguments_(false); 392 + const _directives = directives(false); 393 + let _selectionSet: ast.SelectionSetNode | undefined; 394 + if (input.charCodeAt(idx) === 123 /*'{'*/) { 395 + idx++; 367 396 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 - }); 397 + _selectionSet = selectionSet(); 383 398 } 384 - } else { 385 - throw error('SelectionSet'); 399 + selections.push({ 400 + kind: 'Field' as Kind.FIELD, 401 + alias, 402 + name, 403 + arguments: _arguments, 404 + directives: _directives, 405 + selectionSet: _selectionSet, 406 + }); 386 407 } 387 408 } while (input.charCodeAt(idx) !== 125 /*'}'*/); 388 409 idx++; ··· 399 420 const vars: ast.VariableDefinitionNode[] = []; 400 421 idx++; 401 422 ignored(); 402 - let _name: string | undefined; 403 423 do { 404 424 if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable'); 405 - if ((_name = advance(nameRe)) == null) throw error('Variable'); 406 - ignored(); 425 + const name = nameNode(); 407 426 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition'); 408 427 ignored(); 409 428 const _type = type(); ··· 418 437 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION, 419 438 variable: { 420 439 kind: 'Variable' as Kind.VARIABLE, 421 - name: { kind: 'Name' as Kind.NAME, value: _name }, 440 + name, 422 441 }, 423 442 type: _type, 424 443 defaultValue: _defaultValue, ··· 432 451 } 433 452 434 453 function 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'); 454 + const name = nameNode(); 455 + if (input.charCodeAt(idx++) !== 111 /*'o'*/ || input.charCodeAt(idx++) !== 110 /*'n'*/) 456 + throw error('FragmentDefinition'); 445 457 ignored(); 446 458 return { 447 459 kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, 448 - name: { kind: 'Name' as Kind.NAME, value: _name }, 460 + name, 449 461 typeCondition: { 450 462 kind: 'NamedType' as Kind.NAMED_TYPE, 451 - name: { kind: 'Name' as Kind.NAME, value: _condition }, 463 + name: nameNode(), 452 464 }, 453 - directives: _directives, 454 - selectionSet: selectionSet(), 465 + directives: directives(false), 466 + selectionSet: selectionSetStart(), 455 467 }; 456 468 } 457 469 458 - const definitionRe = /(?:query|mutation|subscription|fragment)/y; 459 - 460 - function 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 - 486 470 function document(input: string, noLoc: boolean): ast.DocumentNode { 487 - let match: string | undefined; 488 - let definition: ast.OperationDefinitionNode | undefined; 489 471 ignored(); 490 472 const definitions: ast.ExecutableDefinitionNode[] = []; 491 473 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); 474 + if (input.charCodeAt(idx) === 123 /*'{'*/) { 475 + definitions.push({ 476 + kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 477 + operation: 'query' as OperationTypeNode.QUERY, 478 + name: undefined, 479 + variableDefinitions: undefined, 480 + directives: undefined, 481 + selectionSet: selectionSetStart(), 482 + }); 497 483 } else { 498 - throw error('Document'); 484 + const definition = name(); 485 + switch (definition) { 486 + case 'fragment': 487 + definitions.push(fragmentDefinition()); 488 + break; 489 + case 'query': 490 + case 'mutation': 491 + case 'subscription': 492 + let char: number; 493 + let name: ast.NameNode | undefined; 494 + if ( 495 + (char = input.charCodeAt(idx)) !== 40 /*'('*/ && 496 + char !== 64 /*'@'*/ && 497 + char !== 123 /*'{'*/ 498 + ) { 499 + name = nameNode(); 500 + } 501 + definitions.push({ 502 + kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 503 + operation: definition as OperationTypeNode, 504 + name, 505 + variableDefinitions: variableDefinitions(), 506 + directives: directives(false), 507 + selectionSet: selectionSetStart(), 508 + }); 509 + break; 510 + default: 511 + throw error('Document'); 512 + } 499 513 } 500 514 } while (idx < input.length); 501 515 ··· 543 557 string: string | Source, 544 558 options?: ParseOptions | undefined 545 559 ): ast.DocumentNode { 546 - input = typeof string.body === 'string' ? string.body : string; 560 + input = string.body ? string.body : string; 547 561 idx = 0; 548 562 return document(input, options && options.noLocation); 549 563 } ··· 552 566 string: string | Source, 553 567 _options?: ParseOptions | undefined 554 568 ): ast.ValueNode { 555 - input = typeof string.body === 'string' ? string.body : string; 569 + input = string.body ? string.body : string; 556 570 idx = 0; 557 571 ignored(); 558 572 return value(false); ··· 562 576 string: string | Source, 563 577 _options?: ParseOptions | undefined 564 578 ): ast.TypeNode { 565 - input = typeof string.body === 'string' ? string.body : string; 579 + input = string.body ? string.body : string; 566 580 idx = 0; 567 581 return type(); 568 582 }