Skip to content

Commit 9fa2fb2

Browse files
author
Daniel Nagy
committed
Add fragment spread arguments syntax
1 parent 998bea6 commit 9fa2fb2

File tree

8 files changed

+243
-3
lines changed

8 files changed

+243
-3
lines changed

src/language/__tests__/parser-test.js

+122
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,128 @@ describe('Parser', () => {
375375
expect(() => parse(document)).to.throw('Syntax Error');
376376
});
377377

378+
it('Experimental: allows parsing fragment spread arguments if `experimentalFragmentVariables` is enabled', () => {
379+
const fragmentSpread = '{ ...Foo(v: $v) }';
380+
381+
expect(() =>
382+
parse(fragmentSpread, { experimentalFragmentVariables: true }),
383+
).to.not.throw();
384+
expect(() => parse(fragmentSpread)).to.throw('Syntax Error');
385+
});
386+
387+
it('Experimental: does not parse arguments on inline fragments', () => {
388+
const inlineFragment = '{ ... on Foo(v: $v) { id } }';
389+
expect(() =>
390+
parse(inlineFragment, { experimentalFragmentVariables: true }),
391+
).to.throw('Syntax Error');
392+
});
393+
394+
it('Experimental: parses fragment spread arguments', () => {
395+
const document = parse('{ ...Foo(v: $v) }', {
396+
experimentalFragmentVariables: true,
397+
});
398+
399+
expect(toJSONDeep(document)).to.have.deep.nested.property(
400+
'definitions[0].selectionSet.selections[0]',
401+
{
402+
arguments: [
403+
{
404+
kind: Kind.ARGUMENT,
405+
loc: { end: 14, start: 9 },
406+
name: {
407+
kind: Kind.NAME,
408+
loc: { end: 10, start: 9 },
409+
value: 'v',
410+
},
411+
value: {
412+
kind: Kind.VARIABLE,
413+
loc: { end: 14, start: 12 },
414+
name: {
415+
kind: Kind.NAME,
416+
loc: { end: 14, start: 13 },
417+
value: 'v',
418+
},
419+
},
420+
},
421+
],
422+
directives: [],
423+
kind: Kind.FRAGMENT_SPREAD,
424+
loc: { end: 15, start: 2 },
425+
name: {
426+
kind: Kind.NAME,
427+
loc: { end: 8, start: 5 },
428+
value: 'Foo',
429+
},
430+
},
431+
);
432+
});
433+
434+
it('Experimental: parses fragment spread arguments and directives', () => {
435+
const document = parse('{ ...Foo(v: $v) @skip(if: true) }', {
436+
experimentalFragmentVariables: true,
437+
});
438+
439+
expect(toJSONDeep(document)).to.have.deep.nested.property(
440+
'definitions[0].selectionSet.selections[0]',
441+
{
442+
arguments: [
443+
{
444+
kind: Kind.ARGUMENT,
445+
loc: { end: 14, start: 9 },
446+
name: {
447+
kind: Kind.NAME,
448+
loc: { end: 10, start: 9 },
449+
value: 'v',
450+
},
451+
value: {
452+
kind: Kind.VARIABLE,
453+
loc: { end: 14, start: 12 },
454+
name: {
455+
kind: Kind.NAME,
456+
loc: { end: 14, start: 13 },
457+
value: 'v',
458+
},
459+
},
460+
},
461+
],
462+
directives: [
463+
{
464+
arguments: [
465+
{
466+
kind: Kind.ARGUMENT,
467+
loc: { end: 30, start: 22 },
468+
name: {
469+
kind: Kind.NAME,
470+
loc: { end: 24, start: 22 },
471+
value: 'if',
472+
},
473+
value: {
474+
kind: Kind.BOOLEAN,
475+
loc: { end: 30, start: 26 },
476+
value: true,
477+
},
478+
},
479+
],
480+
kind: Kind.DIRECTIVE,
481+
loc: { end: 31, start: 16 },
482+
name: {
483+
kind: Kind.NAME,
484+
loc: { end: 21, start: 17 },
485+
value: 'skip',
486+
},
487+
},
488+
],
489+
kind: Kind.FRAGMENT_SPREAD,
490+
loc: { end: 31, start: 2 },
491+
name: {
492+
kind: Kind.NAME,
493+
loc: { end: 8, start: 5 },
494+
value: 'Foo',
495+
},
496+
},
497+
);
498+
});
499+
378500
it('contains location information that only stringifies start/end', () => {
379501
const result = parse('{ id }');
380502

src/language/__tests__/printer-test.js

+43
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,49 @@ describe('Printer: Query document', () => {
147147
`);
148148
});
149149

150+
it('Experimental: correctly prints fragment spread arguments', () => {
151+
const parsed = parse('{ ...Foo(bar: $bar) }', {
152+
experimentalFragmentVariables: true,
153+
});
154+
155+
expect(print(parsed)).to.equal(dedent`
156+
{
157+
...Foo(bar: $bar)
158+
}
159+
`);
160+
});
161+
162+
it('Experimental: correctly prints fragment spread arguments that exceed the max line length', () => {
163+
const parsed = parse(
164+
'{ ...Dogs(breads: ["Boston Terrier", "Poodle", "Beagle"], likesPets: true, likesToys: true) }',
165+
{
166+
experimentalFragmentVariables: true,
167+
},
168+
);
169+
170+
expect(print(parsed)).to.equal(dedent`
171+
{
172+
...Dogs(
173+
breads: ["Boston Terrier", "Poodle", "Beagle"]
174+
likesPets: true
175+
likesToys: true
176+
)
177+
}
178+
`);
179+
});
180+
181+
it('Experimental: correctly prints fragment spread arguments with directives', () => {
182+
const parsed = parse('{ ...Foo(bar: $bar) @skip(if: $isBaz) }', {
183+
experimentalFragmentVariables: true,
184+
});
185+
186+
expect(print(parsed)).to.equal(dedent`
187+
{
188+
...Foo(bar: $bar) @skip(if: $isBaz)
189+
}
190+
`);
191+
});
192+
150193
it('prints kitchen sink', () => {
151194
const printed = print(parse(kitchenSinkQuery));
152195

src/language/__tests__/visitor-test.js

+47
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,53 @@ describe('Visitor', () => {
488488
]);
489489
});
490490

491+
it('Experimental: visits fragment spread arguments', () => {
492+
const ast = parse('{ ...Foo(v: $v) }', {
493+
noLocation: true,
494+
experimentalFragmentVariables: true,
495+
});
496+
497+
const visited = [];
498+
499+
visit(ast, {
500+
enter(node) {
501+
checkVisitorFnArgs(ast, arguments);
502+
visited.push(['enter', node.kind, getValue(node)]);
503+
},
504+
leave(node) {
505+
checkVisitorFnArgs(ast, arguments);
506+
visited.push(['leave', node.kind, getValue(node)]);
507+
},
508+
});
509+
510+
const argumentNode = {
511+
kind: Kind.VARIABLE,
512+
name: { kind: Kind.NAME, loc: undefined, value: 'v' },
513+
loc: undefined,
514+
};
515+
516+
expect(visited).to.deep.equal([
517+
['enter', Kind.DOCUMENT, undefined],
518+
['enter', Kind.OPERATION_DEFINITION, undefined],
519+
['enter', Kind.SELECTION_SET, undefined],
520+
['enter', Kind.FRAGMENT_SPREAD, undefined],
521+
['enter', Kind.NAME, 'Foo'],
522+
['leave', Kind.NAME, 'Foo'],
523+
['enter', Kind.ARGUMENT, argumentNode],
524+
['enter', Kind.NAME, 'v'],
525+
['leave', Kind.NAME, 'v'],
526+
['enter', Kind.VARIABLE, undefined],
527+
['enter', Kind.NAME, 'v'],
528+
['leave', Kind.NAME, 'v'],
529+
['leave', Kind.VARIABLE, undefined],
530+
['leave', Kind.ARGUMENT, argumentNode],
531+
['leave', Kind.FRAGMENT_SPREAD, undefined],
532+
['leave', Kind.SELECTION_SET, undefined],
533+
['leave', Kind.OPERATION_DEFINITION, undefined],
534+
['leave', Kind.DOCUMENT, undefined],
535+
]);
536+
});
537+
491538
it('visits kitchen sink', () => {
492539
const ast = parse(kitchenSinkQuery);
493540
const visited = [];

src/language/ast.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,9 @@ export interface FragmentSpreadNode {
282282
readonly kind: 'FragmentSpread';
283283
readonly loc?: Location;
284284
readonly name: NameNode;
285+
// Note: fragment spread arguments are experimental and may be changed or
286+
// removed in the future.
287+
readonly arguments?: ReadonlyArray<ArgumentNode>;
285288
readonly directives?: ReadonlyArray<DirectiveNode>;
286289
}
287290

src/language/ast.js

+3
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ export type FragmentSpreadNode = {|
316316
+kind: 'FragmentSpread',
317317
+loc?: Location,
318318
+name: NameNode,
319+
// Note: fragment spread arguments are experimental and may be changed or
320+
// removed in the future.
321+
+arguments?: $ReadOnlyArray<ArgumentNode>,
319322
+directives?: $ReadOnlyArray<DirectiveNode>,
320323
|};
321324

src/language/parser.js

+6
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,12 @@ export class Parser {
456456
return {
457457
kind: Kind.FRAGMENT_SPREAD,
458458
name: this.parseFragmentName(),
459+
// Experimental support for fragment variables changes the grammar of a
460+
// FragmentSpreadNode.
461+
arguments:
462+
this._options?.experimentalFragmentVariables ?? false
463+
? this.parseArguments(false)
464+
: undefined,
459465
directives: this.parseDirectives(false),
460466
loc: this.loc(start),
461467
};

src/language/printer.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,18 @@ const printDocASTReducer: any = {
5858

5959
// Fragments
6060

61-
FragmentSpread: ({ name, directives }) =>
62-
'...' + name + wrap(' ', join(directives, ' ')),
61+
// Note: fragment spread arguments are experimental and may be changed or
62+
// removed in the future.
63+
FragmentSpread: ({ name, arguments: args, directives }) => {
64+
const prefix = '...' + name;
65+
let argsLine = prefix + wrap('(', join(args, ', '), ')');
66+
67+
if (argsLine.length > MAX_LINE_LENGTH) {
68+
argsLine = prefix + wrap('(\n', indent(join(args, '\n')), '\n)');
69+
}
70+
71+
return argsLine + wrap(' ', join(directives, ' '));
72+
},
6373

6474
InlineFragment: ({ typeCondition, directives, selectionSet }) =>
6575
join(

src/language/visitor.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ export const QueryDocumentKeys: VisitorKeyMap<ASTKindToNode> = {
6363
Field: ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
6464
Argument: ['name', 'value'],
6565

66-
FragmentSpread: ['name', 'directives'],
66+
FragmentSpread: [
67+
'name',
68+
// Note: fragment spread arguments are experimental and may be changed or
69+
// removed in the future.
70+
'arguments',
71+
'directives',
72+
],
6773
InlineFragment: ['typeCondition', 'directives', 'selectionSet'],
6874
FragmentDefinition: [
6975
'name',

0 commit comments

Comments
 (0)