diff --git a/src/__testUtils__/kitchenSinkQuery.ts b/src/__testUtils__/kitchenSinkQuery.ts index ff989d4b46b..46628b65563 100644 --- a/src/__testUtils__/kitchenSinkQuery.ts +++ b/src/__testUtils__/kitchenSinkQuery.ts @@ -10,7 +10,7 @@ query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery { ...frag @onFragmentSpread } } - + field3! field4? requiredField5: field5! diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index 10d88b8a5cb..b21c1ed9b62 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -160,7 +160,7 @@ function executeQuery( query: string, variableValues?: { [variable: string]: unknown }, ) { - const document = parse(query); + const document = parse(query, { experimentalFragmentArguments: true }); return executeSync({ schema, document, variableValues }); } diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 687180846e4..215e743be7e 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -607,16 +607,32 @@ describe('Parser', () => { expect('loc' in result).to.equal(false); }); - it('allows parsing fragment defined arguments', () => { + it('allows parsing fragment defined variables', () => { const document = 'fragment a($v: Boolean = false) on t { f(v: $v) }'; - expect(() => parse(document)).to.not.throw(); + expect(() => + parse(document, { experimentalFragmentArguments: true }), + ).to.not.throw(); + }); + + it('disallows parsing fragment defined variables without experimental flag', () => { + const document = 'fragment a($v: Boolean = false) on t { f(v: $v) }'; + + expect(() => parse(document)).to.throw(); }); it('allows parsing fragment spread arguments', () => { const document = 'fragment a on t { ...b(v: $v) }'; - expect(() => parse(document)).to.not.throw(); + expect(() => + parse(document, { experimentalFragmentArguments: true }), + ).to.not.throw(); + }); + + it('disallows parsing fragment spread arguments without experimental flag', () => { + const document = 'fragment a on t { ...b(v: $v) }'; + + expect(() => parse(document)).to.throw(); }); it('contains location that can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => { diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts index 94152fe079b..76375cc7a3b 100644 --- a/src/language/__tests__/printer-test.ts +++ b/src/language/__tests__/printer-test.ts @@ -113,6 +113,7 @@ describe('Printer: Query document', () => { it('prints fragment with argument definition directives', () => { const fragmentWithArgumentDefinitionDirective = parse( 'fragment Foo($foo: TestType @test) on TestType @testDirective { id }', + { experimentalFragmentArguments: true }, ); expect(print(fragmentWithArgumentDefinitionDirective)).to.equal(dedent` fragment Foo($foo: TestType @test) on TestType @testDirective { @@ -128,6 +129,7 @@ describe('Printer: Query document', () => { id } `, + { experimentalFragmentArguments: true }, ); expect(print(fragmentWithArgumentDefinition)).to.equal(dedent` fragment Foo($a: ComplexType, $b: Boolean = false) on TestType { @@ -139,6 +141,7 @@ describe('Printer: Query document', () => { it('prints fragment spread with arguments', () => { const fragmentSpreadWithArguments = parse( 'fragment Foo on TestType { ...Bar(a: {x: $x}, b: true) }', + { experimentalFragmentArguments: true }, ); expect(print(fragmentSpreadWithArguments)).to.equal(dedent` fragment Foo on TestType { @@ -150,6 +153,7 @@ describe('Printer: Query document', () => { it('prints fragment spread with multi-line arguments', () => { const fragmentSpreadWithArguments = parse( 'fragment Foo on TestType { ...Bar(a: {x: $x, y: $y, z: $z, xy: $xy}, b: true, c: "a long string extending arguments over max length") }', + { experimentalFragmentArguments: true }, ); expect(print(fragmentSpreadWithArguments)).to.equal(dedent` fragment Foo on TestType { diff --git a/src/language/__tests__/visitor-test.ts b/src/language/__tests__/visitor-test.ts index 7ee17a13a77..0af34fdaddb 100644 --- a/src/language/__tests__/visitor-test.ts +++ b/src/language/__tests__/visitor-test.ts @@ -458,6 +458,7 @@ describe('Visitor', () => { it('visits arguments defined on fragments', () => { const ast = parse('fragment a($v: Boolean = false) on t { f }', { noLocation: true, + experimentalFragmentArguments: true, }); const visited: Array = []; @@ -507,6 +508,7 @@ describe('Visitor', () => { it('visits arguments on fragment spreads', () => { const ast = parse('fragment a on t { ...s(v: false) }', { noLocation: true, + experimentalFragmentArguments: true, }); const visited: Array = []; diff --git a/src/language/parser.ts b/src/language/parser.ts index 5847361e033..b8b14082395 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -91,6 +91,27 @@ export interface ParseOptions { */ maxTokens?: number | undefined; + /** + * EXPERIMENTAL: + * + * If enabled, the parser will understand and parse fragment variable definitions + * and arguments on fragment spreads. Fragment variable definitions will be represented + * in the `variableDefinitions` field of the FragmentDefinitionNode. + * Fragment spread arguments will be represented in the `arguments` field of FragmentSpreadNode. + * + * For example: + * + * ```graphql + * { + * t { ...A(var: true) } + * } + * fragment A($var: Boolean = false) on T { + * ...B(x: $var) + * } + * ``` + */ + experimentalFragmentArguments?: boolean | undefined; + /** * EXPERIMENTAL: * @@ -544,7 +565,10 @@ export class Parser { const hasTypeCondition = this.expectOptionalKeyword('on'); if (!hasTypeCondition && this.peek(TokenKind.NAME)) { const name = this.parseFragmentName(); - if (this.peek(TokenKind.PAREN_L)) { + if ( + this.peek(TokenKind.PAREN_L) && + this._options.experimentalFragmentArguments + ) { return this.node(start, { kind: Kind.FRAGMENT_SPREAD, name, @@ -578,7 +602,9 @@ export class Parser { return this.node(start, { kind: Kind.FRAGMENT_DEFINITION, name: this.parseFragmentName(), - variableDefinitions: this.parseVariableDefinitions(), + variableDefinitions: this._options.experimentalFragmentArguments + ? this.parseVariableDefinitions() + : undefined, typeCondition: (this.expectKeyword('on'), this.parseNamedType()), directives: this.parseDirectives(false), selectionSet: this.parseSelectionSet(), diff --git a/src/utilities/__tests__/TypeInfo-test.ts b/src/utilities/__tests__/TypeInfo-test.ts index 00053fe669e..552b57938b4 100644 --- a/src/utilities/__tests__/TypeInfo-test.ts +++ b/src/utilities/__tests__/TypeInfo-test.ts @@ -519,17 +519,20 @@ describe('visitWithTypeInfo', () => { it('supports traversals of fragment arguments', () => { const typeInfo = new TypeInfo(testSchema); - const ast = parse(` - query { - ...Foo(x: 4) - } + const ast = parse( + ` + query { + ...Foo(x: 4) + } - fragment Foo( - $x: ID! - ) on QueryRoot { - human(id: $x) { name } - } - `); + fragment Foo( + $x: ID! + ) on QueryRoot { + human(id: $x) { name } + } + `, + { experimentalFragmentArguments: true }, + ); const visited: Array = []; visit( diff --git a/src/validation/__tests__/harness.ts b/src/validation/__tests__/harness.ts index 682932d8975..7f478fddfef 100644 --- a/src/validation/__tests__/harness.ts +++ b/src/validation/__tests__/harness.ts @@ -123,7 +123,7 @@ export function expectValidationErrorsWithSchema( rule: ValidationRule, queryStr: string, ): any { - const doc = parse(queryStr); + const doc = parse(queryStr, { experimentalFragmentArguments: true }); const errors = validate(schema, doc, [rule]); return expectJSON(errors); }