diff --git a/src/language/__tests__/parser-test.js b/src/language/__tests__/parser-test.js index 937e901b77..a3486e72ad 100644 --- a/src/language/__tests__/parser-test.js +++ b/src/language/__tests__/parser-test.js @@ -391,6 +391,16 @@ describe('Parser', () => { expect(result.loc).to.equal(undefined); }); + it('Experimental: allows parsing fragment defined variables', () => { + const source = new Source( + 'fragment a($v: Boolean = false) on t { f(v: $v) }', + ); + expect(() => + parse(source, { experimentalFragmentVariables: true }), + ).to.not.throw(); + expect(() => parse(source)).to.throw('Syntax Error'); + }); + it('contains location information that only stringifys start/end', () => { const source = new Source('{ id }'); const result = parse(source); diff --git a/src/language/__tests__/printer-test.js b/src/language/__tests__/printer-test.js index 3100c9f6c4..0339098ac0 100644 --- a/src/language/__tests__/printer-test.js +++ b/src/language/__tests__/printer-test.js @@ -103,6 +103,22 @@ describe('Printer', () => { `); }); + it('Experimental: correctly prints fragment defined variables', () => { + const fragmentWithVariable = parse( + ` + fragment Foo($a: ComplexType, $b: Boolean = false) on TestType { + id + } + `, + { experimentalFragmentVariables: true }, + ); + expect(print(fragmentWithVariable)).to.equal(dedent` + fragment Foo($a: ComplexType, $b: Boolean = false) on TestType { + id + } + `); + }); + const kitchenSink = readFileSync(join(__dirname, '/kitchen-sink.graphql'), { encoding: 'utf8', }); diff --git a/src/language/__tests__/visitor-test.js b/src/language/__tests__/visitor-test.js index 65fbda7460..0799138564 100644 --- a/src/language/__tests__/visitor-test.js +++ b/src/language/__tests__/visitor-test.js @@ -290,6 +290,53 @@ describe('Visitor', () => { ]); }); + it('Experimental: visits variables defined in fragments', () => { + const ast = parse('fragment a($v: Boolean = false) on t { f }', { + experimentalFragmentVariables: true, + }); + const visited = []; + + visit(ast, { + enter(node) { + visited.push(['enter', node.kind, node.value]); + }, + leave(node) { + visited.push(['leave', node.kind, node.value]); + }, + }); + + expect(visited).to.deep.equal([ + ['enter', 'Document', undefined], + ['enter', 'FragmentDefinition', undefined], + ['enter', 'Name', 'a'], + ['leave', 'Name', 'a'], + ['enter', 'VariableDefinition', undefined], + ['enter', 'Variable', undefined], + ['enter', 'Name', 'v'], + ['leave', 'Name', 'v'], + ['leave', 'Variable', undefined], + ['enter', 'NamedType', undefined], + ['enter', 'Name', 'Boolean'], + ['leave', 'Name', 'Boolean'], + ['leave', 'NamedType', undefined], + ['enter', 'BooleanValue', false], + ['leave', 'BooleanValue', false], + ['leave', 'VariableDefinition', undefined], + ['enter', 'NamedType', undefined], + ['enter', 'Name', 't'], + ['leave', 'Name', 't'], + ['leave', 'NamedType', undefined], + ['enter', 'SelectionSet', undefined], + ['enter', 'Field', undefined], + ['enter', 'Name', 'f'], + ['leave', 'Name', 'f'], + ['leave', 'Field', undefined], + ['leave', 'SelectionSet', undefined], + ['leave', 'FragmentDefinition', undefined], + ['leave', 'Document', undefined], + ]); + }); + const kitchenSink = readFileSync(join(__dirname, '/kitchen-sink.graphql'), { encoding: 'utf8', }); diff --git a/src/language/ast.js b/src/language/ast.js index 78c1989494..b5005c209a 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -252,6 +252,9 @@ export type FragmentDefinitionNode = { +kind: 'FragmentDefinition', +loc?: Location, +name: NameNode, + // Note: fragment variable definitions are experimental and may be changed + // or removed in the future. + +variableDefinitions?: $ReadOnlyArray, +typeCondition: NamedTypeNode, +directives?: $ReadOnlyArray, +selectionSet: SelectionSetNode, diff --git a/src/language/parser.js b/src/language/parser.js index 6600180234..e5000932d2 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -117,6 +117,24 @@ export type ParseOptions = { * disables that behavior for performance or testing. */ noLocation?: boolean, + + /** + * EXPERIMENTAL: + * + * If enabled, the parser will understand and parse variable definitions + * contained in a fragment definition. They'll be represented in the + * `variableDefinitions` field of the FragmentDefinitionNode. + * + * The syntax is identical to normal, query-defined variables. For example: + * + * fragment A($var: Boolean = false) on T { + * ... + * } + * + * Note: this feature is experimental and may change or be removed in the + * future. + */ + experimentalFragmentVariables?: boolean, }; /** @@ -488,6 +506,20 @@ function parseFragment( function parseFragmentDefinition(lexer: Lexer<*>): FragmentDefinitionNode { const start = lexer.token; expectKeyword(lexer, 'fragment'); + // Experimental support for defining variables within fragments changes + // the grammar of FragmentDefinition: + // - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet + if (lexer.options.experimentalFragmentVariables) { + return { + kind: FRAGMENT_DEFINITION, + name: parseFragmentName(lexer), + variableDefinitions: parseVariableDefinitions(lexer), + typeCondition: (expectKeyword(lexer, 'on'), parseNamedType(lexer)), + directives: parseDirectives(lexer, false), + selectionSet: parseSelectionSet(lexer), + loc: loc(lexer, start), + }; + } return { kind: FRAGMENT_DEFINITION, name: parseFragmentName(lexer), diff --git a/src/language/printer.js b/src/language/printer.js index 5497745006..8dcf1bba67 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -64,9 +64,17 @@ const printDocASTReducer = { ' ', ), - FragmentDefinition: ({ name, typeCondition, directives, selectionSet }) => - `fragment ${name} on ${typeCondition} ` + - wrap('', join(directives, ' '), ' ') + + FragmentDefinition: ({ + name, + typeCondition, + variableDefinitions, + directives, + selectionSet, + }) => + // Note: fragment variable definitions are experimental and may be changed + // or removed in the future. + `fragment ${name}${wrap('(', join(variableDefinitions, ', '), ')')} ` + + `on ${typeCondition} ${wrap('', join(directives, ' '), ' ')}` + selectionSet, // Value diff --git a/src/language/visitor.js b/src/language/visitor.js index f47ae59b7a..dcba11700c 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -23,7 +23,15 @@ export const QueryDocumentKeys = { FragmentSpread: ['name', 'directives'], InlineFragment: ['typeCondition', 'directives', 'selectionSet'], - FragmentDefinition: ['name', 'typeCondition', 'directives', 'selectionSet'], + FragmentDefinition: [ + 'name', + // Note: fragment variable definitions are experimental and may be changed + // or removed in the future. + 'variableDefinitions', + 'typeCondition', + 'directives', + 'selectionSet', + ], IntValue: [], FloatValue: [],