Skip to content

Commit ce0a4b9

Browse files
sam-swarrleebyron
authored andcommitted
Add experimental support for parsing variable definitions in fragments (#1141)
1 parent f39b0fd commit ce0a4b9

File tree

7 files changed

+128
-4
lines changed

7 files changed

+128
-4
lines changed

src/language/__tests__/parser-test.js

+10
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,16 @@ describe('Parser', () => {
391391
expect(result.loc).to.equal(undefined);
392392
});
393393

394+
it('Experimental: allows parsing fragment defined variables', () => {
395+
const source = new Source(
396+
'fragment a($v: Boolean = false) on t { f(v: $v) }',
397+
);
398+
expect(() =>
399+
parse(source, { experimentalFragmentVariables: true }),
400+
).to.not.throw();
401+
expect(() => parse(source)).to.throw('Syntax Error');
402+
});
403+
394404
it('contains location information that only stringifys start/end', () => {
395405
const source = new Source('{ id }');
396406
const result = parse(source);

src/language/__tests__/printer-test.js

+16
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,22 @@ describe('Printer', () => {
103103
`);
104104
});
105105

106+
it('Experimental: correctly prints fragment defined variables', () => {
107+
const fragmentWithVariable = parse(
108+
`
109+
fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
110+
id
111+
}
112+
`,
113+
{ experimentalFragmentVariables: true },
114+
);
115+
expect(print(fragmentWithVariable)).to.equal(dedent`
116+
fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
117+
id
118+
}
119+
`);
120+
});
121+
106122
const kitchenSink = readFileSync(join(__dirname, '/kitchen-sink.graphql'), {
107123
encoding: 'utf8',
108124
});

src/language/__tests__/visitor-test.js

+47
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,53 @@ describe('Visitor', () => {
290290
]);
291291
});
292292

293+
it('Experimental: visits variables defined in fragments', () => {
294+
const ast = parse('fragment a($v: Boolean = false) on t { f }', {
295+
experimentalFragmentVariables: true,
296+
});
297+
const visited = [];
298+
299+
visit(ast, {
300+
enter(node) {
301+
visited.push(['enter', node.kind, node.value]);
302+
},
303+
leave(node) {
304+
visited.push(['leave', node.kind, node.value]);
305+
},
306+
});
307+
308+
expect(visited).to.deep.equal([
309+
['enter', 'Document', undefined],
310+
['enter', 'FragmentDefinition', undefined],
311+
['enter', 'Name', 'a'],
312+
['leave', 'Name', 'a'],
313+
['enter', 'VariableDefinition', undefined],
314+
['enter', 'Variable', undefined],
315+
['enter', 'Name', 'v'],
316+
['leave', 'Name', 'v'],
317+
['leave', 'Variable', undefined],
318+
['enter', 'NamedType', undefined],
319+
['enter', 'Name', 'Boolean'],
320+
['leave', 'Name', 'Boolean'],
321+
['leave', 'NamedType', undefined],
322+
['enter', 'BooleanValue', false],
323+
['leave', 'BooleanValue', false],
324+
['leave', 'VariableDefinition', undefined],
325+
['enter', 'NamedType', undefined],
326+
['enter', 'Name', 't'],
327+
['leave', 'Name', 't'],
328+
['leave', 'NamedType', undefined],
329+
['enter', 'SelectionSet', undefined],
330+
['enter', 'Field', undefined],
331+
['enter', 'Name', 'f'],
332+
['leave', 'Name', 'f'],
333+
['leave', 'Field', undefined],
334+
['leave', 'SelectionSet', undefined],
335+
['leave', 'FragmentDefinition', undefined],
336+
['leave', 'Document', undefined],
337+
]);
338+
});
339+
293340
const kitchenSink = readFileSync(join(__dirname, '/kitchen-sink.graphql'), {
294341
encoding: 'utf8',
295342
});

src/language/ast.js

+3
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ export type FragmentDefinitionNode = {
255255
+kind: 'FragmentDefinition',
256256
+loc?: Location,
257257
+name: NameNode,
258+
// Note: fragment variable definitions are experimental and may be changed
259+
// or removed in the future.
260+
+variableDefinitions?: $ReadOnlyArray<VariableDefinitionNode>,
258261
+typeCondition: NamedTypeNode,
259262
+directives?: $ReadOnlyArray<DirectiveNode>,
260263
+selectionSet: SelectionSetNode,

src/language/parser.js

+32
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ export type ParseOptions = {
118118
* disables that behavior for performance or testing.
119119
*/
120120
noLocation?: boolean,
121+
122+
/**
123+
* EXPERIMENTAL:
124+
*
125+
* If enabled, the parser will understand and parse variable definitions
126+
* contained in a fragment definition. They'll be represented in the
127+
* `variableDefinitions` field of the FragmentDefinitionNode.
128+
*
129+
* The syntax is identical to normal, query-defined variables. For example:
130+
*
131+
* fragment A($var: Boolean = false) on T {
132+
* ...
133+
* }
134+
*
135+
* Note: this feature is experimental and may change or be removed in the
136+
* future.
137+
*/
138+
experimentalFragmentVariables?: boolean,
121139
};
122140

123141
/**
@@ -504,6 +522,20 @@ function parseFragment(
504522
function parseFragmentDefinition(lexer: Lexer<*>): FragmentDefinitionNode {
505523
const start = lexer.token;
506524
expectKeyword(lexer, 'fragment');
525+
// Experimental support for defining variables within fragments changes
526+
// the grammar of FragmentDefinition:
527+
// - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
528+
if (lexer.options.experimentalFragmentVariables) {
529+
return {
530+
kind: FRAGMENT_DEFINITION,
531+
name: parseFragmentName(lexer),
532+
variableDefinitions: parseVariableDefinitions(lexer),
533+
typeCondition: (expectKeyword(lexer, 'on'), parseNamedType(lexer)),
534+
directives: parseDirectives(lexer, false),
535+
selectionSet: parseSelectionSet(lexer),
536+
loc: loc(lexer, start),
537+
};
538+
}
507539
return {
508540
kind: FRAGMENT_DEFINITION,
509541
name: parseFragmentName(lexer),

src/language/printer.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,17 @@ const printDocASTReducer = {
6464
' ',
6565
),
6666

67-
FragmentDefinition: ({ name, typeCondition, directives, selectionSet }) =>
68-
`fragment ${name} on ${typeCondition} ` +
69-
wrap('', join(directives, ' '), ' ') +
67+
FragmentDefinition: ({
68+
name,
69+
typeCondition,
70+
variableDefinitions,
71+
directives,
72+
selectionSet,
73+
}) =>
74+
// Note: fragment variable definitions are experimental and may be changed
75+
// or removed in the future.
76+
`fragment ${name}${wrap('(', join(variableDefinitions, ', '), ')')} ` +
77+
`on ${typeCondition} ${wrap('', join(directives, ' '), ' ')}` +
7078
selectionSet,
7179

7280
// Value

src/language/visitor.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ export const QueryDocumentKeys = {
2323

2424
FragmentSpread: ['name', 'directives'],
2525
InlineFragment: ['typeCondition', 'directives', 'selectionSet'],
26-
FragmentDefinition: ['name', 'typeCondition', 'directives', 'selectionSet'],
26+
FragmentDefinition: [
27+
'name',
28+
// Note: fragment variable definitions are experimental and may be changed
29+
// or removed in the future.
30+
'variableDefinitions',
31+
'typeCondition',
32+
'directives',
33+
'selectionSet',
34+
],
2735

2836
IntValue: [],
2937
FloatValue: [],

0 commit comments

Comments
 (0)