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

refactor: Improve `parse` performance (#25)

* Combine value matchers into single regex match

* Refactor object/list parser into leading loop

* Turn arguments parsing into leading loop

* Tweak type parsing implementation

* Reformat variableDefinitions parsing

* Refactor selectionSet parsing to merge branches

* Pull out start of selections in parsing branches

* Remove redundant length checks

* Merge variable name into value parser

* Merge float part into main regex

* Absorb object/list parsing into value parser

* Merge field parsing into selectionSet parser

* Squash away typeCondition parser

* Merge fragmentSpread into selectionSet parser

* Remove redundant arrays in operation definitions

* Extract OperationType matching to document parser

* Avoid allocating variableDefinitions array as possible

* Avoid allocating directives if possible

* Avoid allocating arguments array if possible

* Remove name() helper

* Remove redundant ignore() calls

* Add changeset

* Remove redundant import

* Add mising tests and remove redundant branches

* Update comments

* Add additional tests

* Remove recursion from type() parser

authored by kitten.sh and committed by GitHub 6e4df564 3f3b4406

Changed files
+506 -349
.changeset
src
__tests__
+5
.changeset/green-tables-exist.md
··· 1 + --- 2 + '@0no-co/graphql.web': patch 3 + --- 4 + 5 + Improve parser performance.
+1
.gitignore
··· 8 8 coverage/ 9 9 package-lock.json 10 10 .DS_Store 11 + tsconfig.vitest-temp.json
+50 -50
src/__tests__/__snapshots__/parser.test.ts.snap
··· 6 6 { 7 7 "directives": [ 8 8 { 9 - "arguments": [], 9 + "arguments": undefined, 10 10 "kind": "Directive", 11 11 "name": { 12 12 "kind": "Name", ··· 50 50 }, 51 51 }, 52 52 ], 53 - "directives": [], 53 + "directives": undefined, 54 54 "kind": "Field", 55 55 "name": { 56 56 "kind": "Name", ··· 61 61 "selections": [ 62 62 { 63 63 "alias": undefined, 64 - "arguments": [], 65 - "directives": [], 64 + "arguments": undefined, 65 + "directives": undefined, 66 66 "kind": "Field", 67 67 "name": { 68 68 "kind": "Name", ··· 73 73 { 74 74 "directives": [ 75 75 { 76 - "arguments": [], 76 + "arguments": undefined, 77 77 "kind": "Directive", 78 78 "name": { 79 79 "kind": "Name", ··· 87 87 "selections": [ 88 88 { 89 89 "alias": undefined, 90 - "arguments": [], 91 - "directives": [], 90 + "arguments": undefined, 91 + "directives": undefined, 92 92 "kind": "Field", 93 93 "name": { 94 94 "kind": "Name", ··· 99 99 "selections": [ 100 100 { 101 101 "alias": undefined, 102 - "arguments": [], 103 - "directives": [], 102 + "arguments": undefined, 103 + "directives": undefined, 104 104 "kind": "Field", 105 105 "name": { 106 106 "kind": "Name", ··· 175 175 "selections": [ 176 176 { 177 177 "alias": undefined, 178 - "arguments": [], 179 - "directives": [], 178 + "arguments": undefined, 179 + "directives": undefined, 180 180 "kind": "Field", 181 181 "name": { 182 182 "kind": "Name", ··· 187 187 { 188 188 "directives": [ 189 189 { 190 - "arguments": [], 190 + "arguments": undefined, 191 191 "kind": "Directive", 192 192 "name": { 193 193 "kind": "Name", ··· 249 249 "selections": [ 250 250 { 251 251 "alias": undefined, 252 - "arguments": [], 253 - "directives": [], 252 + "arguments": undefined, 253 + "directives": undefined, 254 254 "kind": "Field", 255 255 "name": { 256 256 "kind": "Name", ··· 263 263 "typeCondition": undefined, 264 264 }, 265 265 { 266 - "directives": [], 266 + "directives": undefined, 267 267 "kind": "InlineFragment", 268 268 "selectionSet": { 269 269 "kind": "SelectionSet", 270 270 "selections": [ 271 271 { 272 272 "alias": undefined, 273 - "arguments": [], 274 - "directives": [], 273 + "arguments": undefined, 274 + "directives": undefined, 275 275 "kind": "Field", 276 276 "name": { 277 277 "kind": "Name", ··· 291 291 "variableDefinitions": [ 292 292 { 293 293 "defaultValue": undefined, 294 - "directives": [], 294 + "directives": undefined, 295 295 "kind": "VariableDefinition", 296 296 "type": { 297 297 "kind": "NamedType", ··· 313 313 "kind": "EnumValue", 314 314 "value": "MOBILE", 315 315 }, 316 - "directives": [], 316 + "directives": undefined, 317 317 "kind": "VariableDefinition", 318 318 "type": { 319 319 "kind": "NamedType", ··· 335 335 { 336 336 "directives": [ 337 337 { 338 - "arguments": [], 338 + "arguments": undefined, 339 339 "kind": "Directive", 340 340 "name": { 341 341 "kind": "Name", ··· 369 369 ], 370 370 "directives": [ 371 371 { 372 - "arguments": [], 372 + "arguments": undefined, 373 373 "kind": "Directive", 374 374 "name": { 375 375 "kind": "Name", ··· 387 387 "selections": [ 388 388 { 389 389 "alias": undefined, 390 - "arguments": [], 391 - "directives": [], 390 + "arguments": undefined, 391 + "directives": undefined, 392 392 "kind": "Field", 393 393 "name": { 394 394 "kind": "Name", ··· 399 399 "selections": [ 400 400 { 401 401 "alias": undefined, 402 - "arguments": [], 402 + "arguments": undefined, 403 403 "directives": [ 404 404 { 405 - "arguments": [], 405 + "arguments": undefined, 406 406 "kind": "Directive", 407 407 "name": { 408 408 "kind": "Name", ··· 425 425 }, 426 426 ], 427 427 }, 428 - "variableDefinitions": [], 428 + "variableDefinitions": undefined, 429 429 }, 430 430 { 431 431 "directives": [ 432 432 { 433 - "arguments": [], 433 + "arguments": undefined, 434 434 "kind": "Directive", 435 435 "name": { 436 436 "kind": "Name", ··· 465 465 }, 466 466 }, 467 467 ], 468 - "directives": [], 468 + "directives": undefined, 469 469 "kind": "Field", 470 470 "name": { 471 471 "kind": "Name", ··· 476 476 "selections": [ 477 477 { 478 478 "alias": undefined, 479 - "arguments": [], 480 - "directives": [], 479 + "arguments": undefined, 480 + "directives": undefined, 481 481 "kind": "Field", 482 482 "name": { 483 483 "kind": "Name", ··· 488 488 "selections": [ 489 489 { 490 490 "alias": undefined, 491 - "arguments": [], 492 - "directives": [], 491 + "arguments": undefined, 492 + "directives": undefined, 493 493 "kind": "Field", 494 494 "name": { 495 495 "kind": "Name", ··· 500 500 "selections": [ 501 501 { 502 502 "alias": undefined, 503 - "arguments": [], 504 - "directives": [], 503 + "arguments": undefined, 504 + "directives": undefined, 505 505 "kind": "Field", 506 506 "name": { 507 507 "kind": "Name", ··· 514 514 }, 515 515 { 516 516 "alias": undefined, 517 - "arguments": [], 518 - "directives": [], 517 + "arguments": undefined, 518 + "directives": undefined, 519 519 "kind": "Field", 520 520 "name": { 521 521 "kind": "Name", ··· 526 526 "selections": [ 527 527 { 528 528 "alias": undefined, 529 - "arguments": [], 530 - "directives": [], 529 + "arguments": undefined, 530 + "directives": undefined, 531 531 "kind": "Field", 532 532 "name": { 533 533 "kind": "Name", ··· 549 549 "variableDefinitions": [ 550 550 { 551 551 "defaultValue": undefined, 552 - "directives": [], 552 + "directives": undefined, 553 553 "kind": "VariableDefinition", 554 554 "type": { 555 555 "kind": "NamedType", ··· 571 571 { 572 572 "directives": [ 573 573 { 574 - "arguments": [], 574 + "arguments": undefined, 575 575 "kind": "Directive", 576 576 "name": { 577 577 "kind": "Name", ··· 652 652 }, 653 653 }, 654 654 ], 655 - "directives": [], 655 + "directives": undefined, 656 656 "kind": "Field", 657 657 "name": { 658 658 "kind": "Name", ··· 671 671 }, 672 672 }, 673 673 { 674 - "directives": [], 674 + "directives": undefined, 675 675 "kind": "OperationDefinition", 676 676 "name": { 677 677 "kind": "Name", ··· 717 717 }, 718 718 }, 719 719 ], 720 - "directives": [], 720 + "directives": undefined, 721 721 "kind": "Field", 722 722 "name": { 723 723 "kind": "Name", ··· 727 727 }, 728 728 { 729 729 "alias": undefined, 730 - "arguments": [], 731 - "directives": [], 730 + "arguments": undefined, 731 + "directives": undefined, 732 732 "kind": "Field", 733 733 "name": { 734 734 "kind": "Name", ··· 738 738 }, 739 739 ], 740 740 }, 741 - "variableDefinitions": [], 741 + "variableDefinitions": undefined, 742 742 }, 743 743 { 744 - "directives": [], 744 + "directives": undefined, 745 745 "kind": "OperationDefinition", 746 746 "name": { 747 747 "kind": "Name", ··· 753 753 "selections": [ 754 754 { 755 755 "alias": undefined, 756 - "arguments": [], 757 - "directives": [], 756 + "arguments": undefined, 757 + "directives": undefined, 758 758 "kind": "Field", 759 759 "name": { 760 760 "kind": "Name", ··· 764 764 }, 765 765 ], 766 766 }, 767 - "variableDefinitions": [], 767 + "variableDefinitions": undefined, 768 768 }, 769 769 ], 770 770 "kind": "Document",
+131 -25
src/__tests__/parser.test.ts
··· 1 1 import { describe, it, expect } from 'vitest'; 2 - import * as graphql16 from 'graphql16'; 3 2 4 3 import kitchenSinkDocument from './fixtures/kitchen_sink.graphql?raw'; 5 4 import { parse, parseType, parseValue } from '../parser'; ··· 9 8 it('parses the kitchen sink document like graphql.js does', () => { 10 9 const doc = parse(kitchenSinkDocument); 11 10 expect(doc).toMatchSnapshot(); 12 - expect(doc).toEqual(graphql16.parse(kitchenSinkDocument, { noLocation: true })); 13 11 }); 14 12 15 13 it('parses basic documents', () => { ··· 46 44 }).toThrow(); 47 45 }); 48 46 47 + it('parses directives on fragment spread', () => { 48 + expect(() => parse('{ ...Frag @ }')).toThrow(); 49 + expect(() => parse('{ ...Frag @() }')).toThrow(); 50 + 51 + expect(parse('{ ...Frag @test }')).toHaveProperty( 52 + 'definitions.0.selectionSet.selections.0.directives.0', 53 + { 54 + kind: Kind.DIRECTIVE, 55 + name: { 56 + kind: Kind.NAME, 57 + value: 'test', 58 + }, 59 + arguments: undefined, 60 + } 61 + ); 62 + }); 63 + 64 + it('does not accept empty documents', () => { 65 + expect(() => { 66 + return parse(''); 67 + }).toThrow(); 68 + }); 69 + 70 + it('does not accept incomplete definitions', () => { 71 + expect(() => { 72 + return parse('{} query'); 73 + }).toThrow(); 74 + }); 75 + 49 76 it('parses multi-byte characters', () => { 50 77 // Note: \u0A0A could be naively interpreted as two line-feed chars. 51 78 const ast = parse(` ··· 102 129 it('parses fragment definitions', () => { 103 130 expect(() => parse('fragment { test }')).toThrow(); 104 131 expect(() => parse('fragment name { test }')).toThrow(); 132 + expect(() => parse('fragment name on ')).toThrow(); 105 133 expect(() => parse('fragment name on name')).toThrow(); 106 134 expect(() => parse('fragment Name on Type { field }')).not.toThrow(); 107 135 }); ··· 114 142 'selectionSet.selections.0', 115 143 { 116 144 kind: Kind.FIELD, 117 - directives: [], 118 - arguments: [], 145 + directives: undefined, 146 + arguments: undefined, 119 147 alias: { 120 148 kind: Kind.NAME, 121 149 value: 'alias', ··· 129 157 selections: [ 130 158 { 131 159 kind: Kind.FIELD, 132 - directives: [], 133 - arguments: [], 160 + directives: undefined, 161 + arguments: undefined, 134 162 name: { 135 163 kind: Kind.NAME, 136 164 value: 'child', ··· 145 173 it('parses arguments', () => { 146 174 expect(() => parse('{ field() }')).toThrow(); 147 175 expect(() => parse('{ field(name) }')).toThrow(); 148 - expect(() => parse('{ field(name:) }')).toThrow(); 176 + expect(() => parse('{ field(name: ) }')).toThrow(); 149 177 expect(() => parse('{ field(name: null }')).toThrow(); 178 + expect(() => parse('{ field(name: % )')).toThrow(); 150 179 151 180 expect(parse('{ field(name: null) }').definitions[0]).toMatchObject({ 152 181 kind: Kind.OPERATION_DEFINITION, ··· 177 206 }); 178 207 }); 179 208 180 - it('parses directives', () => { 209 + it('parses directives on fields', () => { 181 210 expect(() => parse('{ field @ }')).toThrow(); 182 211 expect(() => parse('{ field @(test: null) }')).toThrow(); 183 212 ··· 214 243 'definitions.0.selectionSet.selections.0', 215 244 { 216 245 kind: Kind.INLINE_FRAGMENT, 217 - directives: [], 246 + directives: undefined, 218 247 typeCondition: { 219 248 kind: Kind.NAMED_TYPE, 220 249 name: { ··· 228 257 229 258 expect(parse('{ ... { field } }')).toHaveProperty('definitions.0.selectionSet.selections.0', { 230 259 kind: Kind.INLINE_FRAGMENT, 231 - directives: [], 260 + directives: undefined, 232 261 typeCondition: undefined, 233 262 selectionSet: expect.any(Object), 234 263 }); 235 264 }); 236 265 266 + it('parses directives on inline fragments', () => { 267 + expect(() => parse('{ ... @ { field } }')).toThrow(); 268 + expect(() => parse('{ ... @() { field } }')).toThrow(); 269 + 270 + expect(parse('{ field @test { field } }')).toHaveProperty( 271 + 'definitions.0.selectionSet.selections.0.directives.0', 272 + { 273 + kind: Kind.DIRECTIVE, 274 + name: { 275 + kind: Kind.NAME, 276 + value: 'test', 277 + }, 278 + arguments: undefined, 279 + } 280 + ); 281 + }); 282 + 237 283 it('parses variable definitions', () => { 238 284 expect(() => parse('query ( { test }')).toThrow(); 285 + expect(() => parse('query ($) { test }')).toThrow(); 239 286 expect(() => parse('query ($var) { test }')).toThrow(); 240 287 expect(() => parse('query ($var:) { test }')).toThrow(); 241 288 expect(() => parse('query ($var: Int =) { test }')).toThrow(); ··· 243 290 expect(parse('query ($var: Int = 1) { test }').definitions[0]).toMatchObject({ 244 291 kind: Kind.OPERATION_DEFINITION, 245 292 operation: 'query', 246 - directives: [], 293 + directives: undefined, 247 294 selectionSet: expect.any(Object), 248 295 variableDefinitions: [ 249 296 { ··· 271 318 }); 272 319 }); 273 320 321 + it('parses directives on variable definitions', () => { 322 + expect(() => parse('query ($var: Int @) { field }')).toThrow(); 323 + expect(() => parse('query ($var: Int @test()) { field }')).toThrow(); 324 + 325 + expect(parse('query ($var: Int @test) { field }')).toHaveProperty( 326 + 'definitions.0.variableDefinitions.0.directives.0', 327 + { 328 + kind: Kind.DIRECTIVE, 329 + name: { 330 + kind: Kind.NAME, 331 + value: 'test', 332 + }, 333 + arguments: undefined, 334 + } 335 + ); 336 + }); 337 + 274 338 it('creates ast', () => { 275 339 const result = parse(` 276 340 { ··· 288 352 kind: Kind.OPERATION_DEFINITION, 289 353 operation: 'query', 290 354 name: undefined, 291 - variableDefinitions: [], 292 - directives: [], 355 + variableDefinitions: undefined, 356 + directives: undefined, 293 357 selectionSet: { 294 358 kind: Kind.SELECTION_SET, 295 359 selections: [ ··· 313 377 }, 314 378 }, 315 379 ], 316 - directives: [], 380 + directives: undefined, 317 381 selectionSet: { 318 382 kind: Kind.SELECTION_SET, 319 383 selections: [ ··· 324 388 kind: Kind.NAME, 325 389 value: 'id', 326 390 }, 327 - arguments: [], 328 - directives: [], 391 + arguments: undefined, 392 + directives: undefined, 329 393 selectionSet: undefined, 330 394 }, 331 395 { ··· 335 399 kind: Kind.NAME, 336 400 value: 'name', 337 401 }, 338 - arguments: [], 339 - directives: [], 402 + arguments: undefined, 403 + directives: undefined, 340 404 selectionSet: undefined, 341 405 }, 342 406 ], ··· 365 429 kind: Kind.OPERATION_DEFINITION, 366 430 operation: 'query', 367 431 name: undefined, 368 - variableDefinitions: [], 369 - directives: [], 432 + variableDefinitions: undefined, 433 + directives: undefined, 370 434 selectionSet: { 371 435 kind: Kind.SELECTION_SET, 372 436 selections: [ ··· 377 441 kind: Kind.NAME, 378 442 value: 'node', 379 443 }, 380 - arguments: [], 381 - directives: [], 444 + arguments: undefined, 445 + directives: undefined, 382 446 selectionSet: { 383 447 kind: Kind.SELECTION_SET, 384 448 selections: [ ··· 389 453 kind: Kind.NAME, 390 454 value: 'id', 391 455 }, 392 - arguments: [], 393 - directives: [], 456 + arguments: undefined, 457 + directives: undefined, 394 458 selectionSet: undefined, 395 459 }, 396 460 ], ··· 642 706 expect(() => parseType('!')).toThrow(); 643 707 expect(() => parseType('[String')).toThrow(); 644 708 expect(() => parseType('[String!')).toThrow(); 709 + expect(() => parseType('[[String!')).toThrow(); 710 + expect(() => parseType('[[String]!')).toThrow(); 711 + expect(() => parseType('[[String]')).toThrow(); 645 712 }); 646 713 647 714 it('parses well known types', () => { ··· 695 762 }); 696 763 697 764 it('parses nested types', () => { 698 - const result = parseType('[MyType!]'); 765 + let result = parseType('[MyType!]'); 699 766 expect(result).toEqual({ 700 767 kind: Kind.LIST_TYPE, 701 768 type: { ··· 705 772 name: { 706 773 kind: Kind.NAME, 707 774 value: 'MyType', 775 + }, 776 + }, 777 + }, 778 + }); 779 + 780 + result = parseType('[[MyType!]]'); 781 + expect(result).toEqual({ 782 + kind: Kind.LIST_TYPE, 783 + type: { 784 + kind: Kind.LIST_TYPE, 785 + type: { 786 + kind: Kind.NON_NULL_TYPE, 787 + type: { 788 + kind: Kind.NAMED_TYPE, 789 + name: { 790 + kind: Kind.NAME, 791 + value: 'MyType', 792 + }, 793 + }, 794 + }, 795 + }, 796 + }); 797 + 798 + result = parseType('[[MyType!]]!'); 799 + expect(result).toEqual({ 800 + kind: Kind.NON_NULL_TYPE, 801 + type: { 802 + kind: Kind.LIST_TYPE, 803 + type: { 804 + kind: Kind.LIST_TYPE, 805 + type: { 806 + kind: Kind.NON_NULL_TYPE, 807 + type: { 808 + kind: Kind.NAMED_TYPE, 809 + name: { 810 + kind: Kind.NAME, 811 + value: 'MyType', 812 + }, 813 + }, 708 814 }, 709 815 }, 710 816 },
+319 -274
src/parser.ts
··· 66 66 } 67 67 68 68 const nameRe = /[_A-Za-z]\w*/y; 69 - function name(): ast.NameNode | undefined { 70 - let match: string | undefined; 71 - if ((match = advance(nameRe))) { 72 - return { 73 - kind: 'Name' as Kind.NAME, 74 - value: match, 75 - }; 76 - } 77 - } 78 69 79 - // NOTE(Safari10 Quirk): This needs to be wrapped in a non-capturing group 80 - const constRe = /(?:null|true|false)/y; 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 + ); 81 92 82 - const variableRe = /\$[_A-Za-z]\w*/y; 83 - const intRe = /-?\d+/y; 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, 102 + } 84 103 85 - // NOTE(Safari10 Quirk): This cannot be further simplified 86 - const floatPartRe = /(?:\.\d+)?[eE][+-]?\d+|\.\d+/y; 104 + type ValueExec = RegExpExecArray & { 105 + [Prop in ValueGroup]: string | undefined; 106 + }; 87 107 88 108 const complexStringRe = /\\/g; 89 - const blockStringRe = /"""(?:"""|(?:[\s\S]*?[^\\])""")/y; 90 - const stringRe = /"(?:"|[^\r\n]*?[^\\]")/y; 91 109 92 110 function value(constant: true): ast.ConstValueNode; 93 111 function value(constant: boolean): ast.ValueNode; 94 112 95 - function value(constant: boolean): ast.ValueNode | undefined { 96 - let out: ast.ValueNode | undefined; 113 + function value(constant: boolean): ast.ValueNode { 97 114 let match: string | undefined; 98 - if ((match = advance(constRe))) { 99 - out = 100 - match === 'null' 101 - ? { 102 - kind: 'NullValue' as Kind.NULL, 103 - } 104 - : { 105 - kind: 'BooleanValue' as Kind.BOOLEAN, 106 - value: match === 'true', 107 - }; 108 - } else if (!constant && (match = advance(variableRe))) { 109 - out = { 110 - kind: 'Variable' as Kind.VARIABLE, 111 - name: { 112 - kind: 'Name' as Kind.NAME, 113 - value: match.slice(1), 114 - }, 115 - }; 116 - } else if ((match = advance(intRe))) { 117 - const intPart = match; 118 - if ((match = advance(floatPartRe))) { 119 - out = { 120 - kind: 'FloatValue' as Kind.FLOAT, 121 - value: intPart + match, 122 - }; 123 - } else { 124 - out = { 125 - kind: 'IntValue' as Kind.INT, 126 - value: intPart, 127 - }; 128 - } 129 - } else if ((match = advance(nameRe))) { 130 - out = { 131 - kind: 'EnumValue' as Kind.ENUM, 132 - value: match, 133 - }; 134 - } else if ((match = advance(blockStringRe))) { 135 - out = { 136 - kind: 'StringValue' as Kind.STRING, 137 - value: blockString(match.slice(3, -3)), 138 - block: true, 139 - }; 140 - } else if ((match = advance(stringRe))) { 141 - out = { 142 - kind: 'StringValue' as Kind.STRING, 143 - value: complexStringRe.test(match) ? (JSON.parse(match) as string) : match.slice(1, -1), 144 - block: false, 145 - }; 146 - } else if ((out = list(constant) || object(constant))) { 147 - return out; 148 - } 149 - 150 - ignored(); 151 - return out; 152 - } 153 - 154 - function list(constant: boolean): ast.ListValueNode | undefined { 155 - let match: ast.ValueNode | undefined; 115 + let exec: ValueExec | null; 116 + valueRe.lastIndex = idx; 156 117 if (input.charCodeAt(idx) === 91 /*'['*/) { 118 + // Lists are checked ahead of time with `[` chars 157 119 idx++; 158 120 ignored(); 159 121 const values: ast.ValueNode[] = []; 160 - while ((match = value(constant))) values.push(match); 161 - if (input.charCodeAt(idx++) !== 93 /*']'*/) throw error('ListValue'); 122 + while (input.charCodeAt(idx) !== 93 /*']'*/) values.push(value(constant)); 123 + idx++; 162 124 ignored(); 163 125 return { 164 126 kind: 'ListValue' as Kind.LIST, 165 127 values, 166 128 }; 167 - } 168 - } 169 - 170 - function object(constant: boolean): ast.ObjectValueNode | undefined { 171 - if (input.charCodeAt(idx) === 123 /*'{'*/) { 129 + } else if (input.charCodeAt(idx) === 123 /*'{'*/) { 130 + // Objects are checked ahead of time with `{` chars 172 131 idx++; 173 132 ignored(); 174 133 const fields: ast.ObjectFieldNode[] = []; 175 - let _name: ast.NameNode | undefined; 176 - while ((_name = name())) { 134 + while (input.charCodeAt(idx) !== 125 /*'}'*/) { 135 + if ((match = advance(nameRe)) == null) throw error('ObjectField'); 177 136 ignored(); 178 - if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField' as Kind.OBJECT_FIELD); 137 + if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField'); 179 138 ignored(); 180 - const _value = value(constant); 181 - if (!_value) throw error('ObjectField'); 182 139 fields.push({ 183 140 kind: 'ObjectField' as Kind.OBJECT_FIELD, 184 - name: _name, 185 - value: _value, 141 + name: { kind: 'Name' as Kind.NAME, value: match }, 142 + value: value(constant), 186 143 }); 187 144 } 188 - if (input.charCodeAt(idx++) !== 125 /*'}'*/) throw error('ObjectValue'); 145 + idx++; 189 146 ignored(); 190 147 return { 191 148 kind: 'ObjectValue' as Kind.OBJECT, 192 149 fields, 193 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 + } 194 207 } 208 + 209 + throw error('Value'); 195 210 } 196 211 197 - function arguments_(constant: boolean): ast.ArgumentNode[] { 198 - const args: ast.ArgumentNode[] = []; 199 - ignored(); 212 + function arguments_(constant: boolean): ast.ArgumentNode[] | undefined { 200 213 if (input.charCodeAt(idx) === 40 /*'('*/) { 214 + const args: ast.ArgumentNode[] = []; 201 215 idx++; 202 216 ignored(); 203 - let _name: ast.NameNode | undefined; 204 - while ((_name = name())) { 217 + let _name: string | undefined; 218 + do { 219 + if ((_name = advance(nameRe)) == null) throw error('Argument'); 205 220 ignored(); 206 221 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument'); 207 222 ignored(); 208 - const _value = value(constant); 209 - if (!_value) throw error('Argument'); 210 223 args.push({ 211 224 kind: 'Argument' as Kind.ARGUMENT, 212 - name: _name, 213 - value: _value, 225 + name: { kind: 'Name' as Kind.NAME, value: _name }, 226 + value: value(constant), 214 227 }); 215 - } 216 - if (!args.length || input.charCodeAt(idx++) !== 41 /*')'*/) throw error('Argument'); 217 - ignored(); 218 - } 219 - return args; 220 - } 221 - 222 - function directives(constant: true): ast.ConstDirectiveNode[]; 223 - function directives(constant: boolean): ast.DirectiveNode[]; 224 - 225 - function directives(constant: boolean): ast.DirectiveNode[] { 226 - const directives: ast.DirectiveNode[] = []; 227 - ignored(); 228 - while (input.charCodeAt(idx) === 64 /*'@'*/) { 228 + } while (input.charCodeAt(idx) !== 41 /*')'*/); 229 229 idx++; 230 - const _name = name(); 231 - if (!_name) throw error('Directive'); 232 230 ignored(); 233 - directives.push({ 234 - kind: 'Directive' as Kind.DIRECTIVE, 235 - name: _name, 236 - arguments: arguments_(constant), 237 - }); 231 + return args; 238 232 } 239 - return directives; 240 233 } 241 234 242 - function field(): ast.FieldNode | undefined { 243 - let _name = name(); 244 - if (_name) { 245 - ignored(); 246 - let _alias: ast.NameNode | undefined; 247 - if (input.charCodeAt(idx) === 58 /*':'*/) { 235 + function directives(constant: true): ast.ConstDirectiveNode[] | undefined; 236 + function directives(constant: boolean): ast.DirectiveNode[] | undefined; 237 + 238 + function directives(constant: boolean): ast.DirectiveNode[] | undefined { 239 + if (input.charCodeAt(idx) === 64 /*'@'*/) { 240 + const directives: ast.DirectiveNode[] = []; 241 + let _name: string | undefined; 242 + do { 248 243 idx++; 244 + if ((_name = advance(nameRe)) == null) throw error('Directive'); 249 245 ignored(); 250 - _alias = _name; 251 - _name = name(); 252 - if (!_name) throw error('Field'); 253 - ignored(); 254 - } 255 - return { 256 - kind: 'Field' as Kind.FIELD, 257 - alias: _alias, 258 - name: _name, 259 - arguments: arguments_(false), 260 - directives: directives(false), 261 - selectionSet: selectionSet(), 262 - }; 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; 263 253 } 264 254 } 265 255 266 256 function type(): ast.TypeNode { 267 - let match: ast.NameNode | ast.TypeNode | undefined; 268 - ignored(); 269 - if (input.charCodeAt(idx) === 91 /*'['*/) { 257 + let match: string | undefined; 258 + let lists = 0; 259 + while (input.charCodeAt(idx) === 91 /*'['*/) { 260 + lists++; 270 261 idx++; 271 262 ignored(); 272 - const _type = type(); 273 - if (!_type || input.charCodeAt(idx++) !== 93 /*']'*/) throw error('ListType'); 274 - match = { 275 - kind: 'ListType' as Kind.LIST_TYPE, 276 - type: _type, 277 - }; 278 - } else if ((match = name())) { 279 - match = { 280 - kind: 'NamedType' as Kind.NAMED_TYPE, 281 - name: match, 282 - }; 283 - } else { 284 - throw error('NamedType'); 285 263 } 286 - 264 + if ((match = advance(nameRe)) == null) throw error('NamedType'); 287 265 ignored(); 288 - if (input.charCodeAt(idx) === 33 /*'!'*/) { 289 - idx++; 290 - ignored(); 291 - return { 292 - kind: 'NonNullType' as Kind.NON_NULL_TYPE, 293 - type: match, 294 - }; 295 - } else { 296 - return match; 297 - } 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; 298 289 } 299 290 300 - const typeConditionRe = /on/y; 301 - function typeCondition(): ast.NamedTypeNode | undefined { 302 - if (advance(typeConditionRe)) { 303 - ignored(); 304 - const _name = name(); 305 - if (!_name) throw error('NamedType'); 306 - ignored(); 307 - return { 308 - kind: 'NamedType' as Kind.NAMED_TYPE, 309 - name: _name, 310 - }; 311 - } 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 + '(\\.\\.\\.)|' + 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, 312 308 } 313 309 314 - const fragmentSpreadRe = /\.\.\./y; 310 + type SelectionExec = RegExpExecArray & { 311 + [Prop in SelectionGroup]: string | undefined; 312 + }; 315 313 316 - function fragmentSpread(): ast.FragmentSpreadNode | ast.InlineFragmentNode | undefined { 317 - if (advance(fragmentSpreadRe)) { 318 - ignored(); 319 - const _idx = idx; 320 - let _name: ast.NameNode | undefined; 321 - if ((_name = name()) && _name.value !== 'on') { 322 - return { 323 - kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, 324 - name: _name, 325 - directives: directives(false), 326 - }; 314 + function 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 + } 365 + const _arguments = arguments_(false); 366 + ignored(); 367 + const _directives = directives(false); 368 + let _selectionSet: ast.SelectionSetNode | undefined; 369 + if (input.charCodeAt(idx) === 123 /*'{'*/) { 370 + idx++; 371 + ignored(); 372 + _selectionSet = selectionSet(); 373 + } 374 + selections.push({ 375 + kind: 'Field' as Kind.FIELD, 376 + alias: _alias ? { kind: 'Name' as Kind.NAME, value: _alias } : undefined, 377 + name: { kind: 'Name' as Kind.NAME, value: match }, 378 + arguments: _arguments, 379 + directives: _directives, 380 + selectionSet: _selectionSet, 381 + }); 382 + } 327 383 } else { 328 - idx = _idx; 329 - const _typeCondition = typeCondition(); 330 - const _directives = directives(false); 331 - const _selectionSet = selectionSet(); 332 - if (!_selectionSet) throw error('InlineFragment'); 333 - return { 334 - kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, 335 - typeCondition: _typeCondition, 336 - directives: _directives, 337 - selectionSet: _selectionSet, 338 - }; 384 + throw error('SelectionSet'); 339 385 } 340 - } 341 - } 342 - 343 - function selectionSet(): ast.SelectionSetNode | undefined { 344 - let match: ast.SelectionNode | undefined; 386 + } while (input.charCodeAt(idx) !== 125 /*'}'*/); 387 + idx++; 345 388 ignored(); 346 - if (input.charCodeAt(idx) === 123 /*'{'*/) { 347 - idx++; 348 - ignored(); 349 - const selections: ast.SelectionNode[] = []; 350 - while ((match = fragmentSpread() || field())) selections.push(match); 351 - if (!selections.length || input.charCodeAt(idx++) !== 125 /*'}'*/) throw error('SelectionSet'); 352 - ignored(); 353 - return { 354 - kind: 'SelectionSet' as Kind.SELECTION_SET, 355 - selections, 356 - }; 357 - } 389 + return { 390 + kind: 'SelectionSet' as Kind.SELECTION_SET, 391 + selections, 392 + }; 358 393 } 359 394 360 - function variableDefinitions(): ast.VariableDefinitionNode[] { 361 - let match: string | undefined; 362 - const vars: ast.VariableDefinitionNode[] = []; 395 + function variableDefinitions(): ast.VariableDefinitionNode[] | undefined { 363 396 ignored(); 364 397 if (input.charCodeAt(idx) === 40 /*'('*/) { 398 + const vars: ast.VariableDefinitionNode[] = []; 365 399 idx++; 366 400 ignored(); 367 - while ((match = advance(variableRe))) { 401 + let _name: string | undefined; 402 + do { 403 + if (input.charCodeAt(idx++) !== 36 /*'$'*/) throw error('Variable'); 404 + if ((_name = advance(nameRe)) == null) throw error('Variable'); 368 405 ignored(); 369 406 if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('VariableDefinition'); 407 + ignored(); 370 408 const _type = type(); 371 - let _defaultValue: ast.ValueNode | undefined; 409 + let _defaultValue: ast.ConstValueNode | undefined; 372 410 if (input.charCodeAt(idx) === 61 /*'='*/) { 373 411 idx++; 374 412 ignored(); 375 413 _defaultValue = value(true); 376 - if (!_defaultValue) throw error('VariableDefinition'); 377 414 } 378 415 ignored(); 379 416 vars.push({ 380 417 kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION, 381 418 variable: { 382 419 kind: 'Variable' as Kind.VARIABLE, 383 - name: { 384 - kind: 'Name' as Kind.NAME, 385 - value: match.slice(1), 386 - }, 420 + name: { kind: 'Name' as Kind.NAME, value: _name }, 387 421 }, 388 422 type: _type, 389 - defaultValue: _defaultValue as ast.ConstValueNode, 423 + defaultValue: _defaultValue, 390 424 directives: directives(true), 391 425 }); 392 - } 393 - if (input.charCodeAt(idx++) !== 41 /*')'*/) throw error('VariableDefinition'); 426 + } while (input.charCodeAt(idx) !== 41 /*')'*/); 427 + idx++; 394 428 ignored(); 429 + return vars; 395 430 } 396 - return vars; 397 431 } 398 432 399 - const fragmentDefinitionRe = /fragment/y; 400 - function fragmentDefinition(): ast.FragmentDefinitionNode | undefined { 401 - if (advance(fragmentDefinitionRe)) { 402 - ignored(); 403 - const _name = name(); 404 - if (!_name) throw error('FragmentDefinition'); 405 - ignored(); 406 - const _typeCondition = typeCondition(); 407 - if (!_typeCondition) throw error('FragmentDefinition'); 408 - const _directives = directives(false); 409 - const _selectionSet = selectionSet(); 410 - if (!_selectionSet) throw error('FragmentDefinition'); 411 - return { 412 - kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, 413 - name: _name, 414 - typeCondition: _typeCondition, 415 - directives: _directives, 416 - selectionSet: _selectionSet, 417 - }; 418 - } 433 + function fragmentDefinition(): ast.FragmentDefinitionNode { 434 + let _name: string | undefined; 435 + let _condition: string | undefined; 436 + if ((_name = advance(nameRe)) == null) throw error('FragmentDefinition'); 437 + ignored(); 438 + if (advance(nameRe) !== 'on') throw error('FragmentDefinition'); 439 + ignored(); 440 + if ((_condition = advance(nameRe)) == null) throw error('FragmentDefinition'); 441 + ignored(); 442 + const _directives = directives(false); 443 + if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('FragmentDefinition'); 444 + ignored(); 445 + return { 446 + kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, 447 + name: { kind: 'Name' as Kind.NAME, value: _name }, 448 + typeCondition: { 449 + kind: 'NamedType' as Kind.NAMED_TYPE, 450 + name: { kind: 'Name' as Kind.NAME, value: _condition }, 451 + }, 452 + directives: _directives, 453 + selectionSet: selectionSet(), 454 + }; 419 455 } 420 456 421 - // NOTE(Safari10 Quirk): This *might* need to be wrapped in a group, but worked without it too 422 - const operationDefinitionRe = /(?:query|mutation|subscription)/y; 457 + const definitionRe = /(?:query|mutation|subscription|fragment)/y; 423 458 424 - function operationDefinition(): ast.OperationDefinitionNode | undefined { 425 - let _operation: string | undefined; 426 - let _name: ast.NameNode | undefined; 427 - let _variableDefinitions: ast.VariableDefinitionNode[] = []; 428 - let _directives: ast.DirectiveNode[] = []; 429 - if ((_operation = advance(operationDefinitionRe))) { 459 + function operationDefinition( 460 + operation: OperationTypeNode | undefined 461 + ): ast.OperationDefinitionNode | undefined { 462 + let _name: string | undefined; 463 + let _variableDefinitions: ast.VariableDefinitionNode[] | undefined; 464 + let _directives: ast.DirectiveNode[] | undefined; 465 + if (operation) { 430 466 ignored(); 431 - _name = name(); 467 + _name = advance(nameRe); 432 468 _variableDefinitions = variableDefinitions(); 433 469 _directives = directives(false); 434 470 } 435 - const _selectionSet = selectionSet(); 436 - if (_selectionSet) { 471 + if (input.charCodeAt(idx) === 123 /*'{'*/) { 472 + idx++; 473 + ignored(); 437 474 return { 438 475 kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, 439 - operation: (_operation || 'query') as OperationTypeNode, 440 - name: _name, 476 + operation: operation || ('query' as OperationTypeNode.QUERY), 477 + name: _name ? { kind: 'Name' as Kind.NAME, value: _name } : undefined, 441 478 variableDefinitions: _variableDefinitions, 442 479 directives: _directives, 443 - selectionSet: _selectionSet, 480 + selectionSet: selectionSet(), 444 481 }; 445 482 } 446 483 } 447 484 448 485 function document(): ast.DocumentNode { 449 - let match: ast.ExecutableDefinitionNode | void; 486 + let match: string | undefined; 487 + let definition: ast.OperationDefinitionNode | undefined; 450 488 ignored(); 451 489 const definitions: ast.ExecutableDefinitionNode[] = []; 452 - while ((match = fragmentDefinition() || operationDefinition())) definitions.push(match); 490 + do { 491 + if ((match = advance(definitionRe)) === 'fragment') { 492 + ignored(); 493 + definitions.push(fragmentDefinition()); 494 + } else if ((definition = operationDefinition(match as OperationTypeNode)) != null) { 495 + definitions.push(definition); 496 + } else { 497 + throw error('Document'); 498 + } 499 + } while (idx < input.length); 453 500 return { 454 501 kind: 'Document' as Kind.DOCUMENT, 455 502 definitions, ··· 476 523 input = typeof string.body === 'string' ? string.body : string; 477 524 idx = 0; 478 525 ignored(); 479 - const _value = value(false); 480 - if (!_value) throw error('ValueNode'); 481 - return _value; 526 + return value(false); 482 527 } 483 528 484 529 export function parseType(