Skip to content

Commit 6b20b31

Browse files
committed
Support printing and parsing
1 parent 9a91e33 commit 6b20b31

File tree

14 files changed

+198
-43
lines changed

14 files changed

+198
-43
lines changed

src/language/__tests__/parser-test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,15 +395,35 @@ describe('Parser', () => {
395395
expect('loc' in result).to.equal(false);
396396
});
397397

398-
it('Legacy: allows parsing fragment defined variables', () => {
398+
it('allows parsing fragment defined variables', () => {
399399
const document = 'fragment a($v: Boolean = false) on t { f(v: $v) }';
400400

401401
expect(() =>
402-
parse(document, { allowLegacyFragmentVariables: true }),
402+
parse(document, { experimentalFragmentArguments: true }),
403403
).to.not.throw();
404404
expect(() => parse(document)).to.throw('Syntax Error');
405405
});
406406

407+
it('disallows parsing fragment defined variables without experimental flag', () => {
408+
const document = 'fragment a($v: Boolean = false) on t { f(v: $v) }';
409+
410+
expect(() => parse(document)).to.throw();
411+
});
412+
413+
it('allows parsing fragment spread arguments', () => {
414+
const document = 'fragment a on t { ...b(v: $v) }';
415+
416+
expect(() =>
417+
parse(document, { experimentalFragmentArguments: true }),
418+
).to.not.throw();
419+
});
420+
421+
it('disallows parsing fragment spread arguments without experimental flag', () => {
422+
const document = 'fragment a on t { ...b(v: $v) }';
423+
424+
expect(() => parse(document)).to.throw();
425+
});
426+
407427
it('contains location that can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => {
408428
const { loc } = parse('{ id }');
409429

src/language/__tests__/printer-test.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ describe('Printer: Query document', () => {
110110
`);
111111
});
112112

113-
it('Legacy: prints fragment with variable directives', () => {
113+
it('prints fragment with variable directives', () => {
114114
const queryASTWithVariableDirective = parse(
115115
'fragment Foo($foo: TestType @test) on TestType @testDirective { id }',
116-
{ allowLegacyFragmentVariables: true },
116+
{ experimentalFragmentArguments: true },
117117
);
118118
expect(print(queryASTWithVariableDirective)).to.equal(dedent`
119119
fragment Foo($foo: TestType @test) on TestType @testDirective {
@@ -122,14 +122,14 @@ describe('Printer: Query document', () => {
122122
`);
123123
});
124124

125-
it('Legacy: correctly prints fragment defined variables', () => {
125+
it('correctly prints fragment defined variables', () => {
126126
const fragmentWithVariable = parse(
127127
`
128128
fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
129129
id
130130
}
131131
`,
132-
{ allowLegacyFragmentVariables: true },
132+
{ experimentalFragmentArguments: true },
133133
);
134134
expect(print(fragmentWithVariable)).to.equal(dedent`
135135
fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
@@ -213,4 +213,33 @@ describe('Printer: Query document', () => {
213213
`),
214214
);
215215
});
216+
217+
it('prints fragment spread with arguments', () => {
218+
const fragmentSpreadWithArguments = parse(
219+
'fragment Foo on TestType { ...Bar(a: {x: $x}, b: true) }',
220+
{ experimentalFragmentArguments: true },
221+
);
222+
expect(print(fragmentSpreadWithArguments)).to.equal(dedent`
223+
fragment Foo on TestType {
224+
...Bar(a: {x: $x}, b: true)
225+
}
226+
`);
227+
});
228+
229+
it('prints fragment spread with multi-line arguments', () => {
230+
const fragmentSpreadWithArguments = parse(
231+
'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") }',
232+
{ experimentalFragmentArguments: true },
233+
);
234+
expect(print(fragmentSpreadWithArguments)).to.equal(dedent`
235+
fragment Foo on TestType {
236+
...Bar(
237+
a: {x: $x, y: $y, z: $z, xy: $xy}
238+
b: true
239+
c: "a long string extending arguments over max length"
240+
)
241+
}
242+
`
243+
);
244+
});
216245
});

src/language/__tests__/visitor-test.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,10 +455,10 @@ describe('Visitor', () => {
455455
]);
456456
});
457457

458-
it('Legacy: visits variables defined in fragments', () => {
458+
it('visits variables defined in fragments', () => {
459459
const ast = parse('fragment a($v: Boolean = false) on t { f }', {
460460
noLocation: true,
461-
allowLegacyFragmentVariables: true,
461+
experimentalFragmentArguments: true,
462462
});
463463
const visited: Array<any> = [];
464464

@@ -1361,4 +1361,48 @@ describe('Visitor', () => {
13611361
]);
13621362
});
13631363
});
1364+
1365+
it('visits arguments on fragment spreads', () => {
1366+
const ast = parse('fragment a on t { ...s(v: false) }', {
1367+
noLocation: true,
1368+
experimentalFragmentArguments: true,
1369+
});
1370+
const visited: Array<any> = [];
1371+
1372+
visit(ast, {
1373+
enter(node) {
1374+
checkVisitorFnArgs(ast, arguments);
1375+
visited.push(['enter', node.kind, getValue(node)]);
1376+
},
1377+
leave(node) {
1378+
checkVisitorFnArgs(ast, arguments);
1379+
visited.push(['leave', node.kind, getValue(node)]);
1380+
},
1381+
});
1382+
1383+
expect(visited).to.deep.equal([
1384+
['enter', 'Document', undefined],
1385+
['enter', 'FragmentDefinition', undefined],
1386+
['enter', 'Name', 'a'],
1387+
['leave', 'Name', 'a'],
1388+
['enter', 'NamedType', undefined],
1389+
['enter', 'Name', 't'],
1390+
['leave', 'Name', 't'],
1391+
['leave', 'NamedType', undefined],
1392+
['enter', 'SelectionSet', undefined],
1393+
['enter', 'FragmentSpread', undefined],
1394+
['enter', 'Name', 's'],
1395+
['leave', 'Name', 's'],
1396+
['enter', 'Argument', { kind: 'BooleanValue', value: false }],
1397+
['enter', 'Name', 'v'],
1398+
['leave', 'Name', 'v'],
1399+
['enter', 'BooleanValue', false],
1400+
['leave', 'BooleanValue', false],
1401+
['leave', 'Argument', { kind: 'BooleanValue', value: false }],
1402+
['leave', 'FragmentSpread', undefined],
1403+
['leave', 'SelectionSet', undefined],
1404+
['leave', 'FragmentDefinition', undefined],
1405+
['leave', 'Document', undefined],
1406+
]);
1407+
});
13641408
});

src/language/ast.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,10 @@ export const QueryDocumentKeys: {
209209
Field: ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
210210
Argument: ['name', 'value'],
211211

212-
FragmentSpread: ['name', 'directives'],
212+
FragmentSpread: ['name', 'arguments', 'directives'],
213213
InlineFragment: ['typeCondition', 'directives', 'selectionSet'],
214214
FragmentDefinition: [
215215
'name',
216-
// Note: fragment variable definitions are deprecated and will removed in v17.0.0
217216
'variableDefinitions',
218217
'typeCondition',
219218
'directives',
@@ -383,6 +382,7 @@ export interface FragmentSpreadNode {
383382
readonly kind: Kind.FRAGMENT_SPREAD;
384383
readonly loc?: Location;
385384
readonly name: NameNode;
385+
readonly arguments?: ReadonlyArray<ArgumentNode>;
386386
readonly directives?: ReadonlyArray<DirectiveNode>;
387387
}
388388

@@ -398,7 +398,6 @@ export interface FragmentDefinitionNode {
398398
readonly kind: Kind.FRAGMENT_DEFINITION;
399399
readonly loc?: Location;
400400
readonly name: NameNode;
401-
/** @deprecated variableDefinitions will be removed in v17.0.0 */
402401
readonly variableDefinitions?: ReadonlyArray<VariableDefinitionNode>;
403402
readonly typeCondition: NamedTypeNode;
404403
readonly directives?: ReadonlyArray<DirectiveNode>;

src/language/directiveLocation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ enum DirectiveLocation {
2323
ENUM_VALUE = 'ENUM_VALUE',
2424
INPUT_OBJECT = 'INPUT_OBJECT',
2525
INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION',
26+
FRAGMENT_VARIABLE_DEFINITION = 'FRAGMENT_VARIABLE_DEFINITION',
2627
}
2728
export { DirectiveLocation };
2829

src/language/parser.ts

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,27 @@ export interface ParseOptions {
103103
* ```
104104
*/
105105
allowLegacyFragmentVariables?: boolean;
106+
107+
/**
108+
* EXPERIMENTAL:
109+
*
110+
* If enabled, the parser will understand and parse fragment variable definitions
111+
* and arguments on fragment spreads. Fragment variable definitions will be represented
112+
* in the `variableDefinitions` field of the FragmentDefinitionNode.
113+
* Fragment spread arguments will be represented in the `arguments` field of FragmentSpreadNode.
114+
*
115+
* For example:
116+
*
117+
* ```graphql
118+
* {
119+
* t { ...A(var: true) }
120+
* }
121+
* fragment A($var: Boolean = false) on T {
122+
* ...B(x: $var)
123+
* }
124+
* ```
125+
*/
126+
experimentalFragmentArguments?: boolean | undefined;
106127
}
107128

108129
/**
@@ -485,7 +506,7 @@ export class Parser {
485506
/**
486507
* Corresponds to both FragmentSpread and InlineFragment in the spec.
487508
*
488-
* FragmentSpread : ... FragmentName Directives?
509+
* FragmentSpread : ... FragmentName Arguments? Directives?
489510
*
490511
* InlineFragment : ... TypeCondition? Directives? SelectionSet
491512
*/
@@ -495,9 +516,21 @@ export class Parser {
495516

496517
const hasTypeCondition = this.expectOptionalKeyword('on');
497518
if (!hasTypeCondition && this.peek(TokenKind.NAME)) {
519+
const name = this.parseFragmentName();
520+
if (
521+
this.peek(TokenKind.PAREN_L) &&
522+
this._options.experimentalFragmentArguments
523+
) {
524+
return this.node<FragmentSpreadNode>(start, {
525+
kind: Kind.FRAGMENT_SPREAD,
526+
name,
527+
arguments: this.parseArguments(false),
528+
directives: this.parseDirectives(false),
529+
});
530+
}
498531
return this.node<FragmentSpreadNode>(start, {
499532
kind: Kind.FRAGMENT_SPREAD,
500-
name: this.parseFragmentName(),
533+
name,
501534
directives: this.parseDirectives(false),
502535
});
503536
}
@@ -511,32 +544,24 @@ export class Parser {
511544

512545
/**
513546
* FragmentDefinition :
514-
* - fragment FragmentName on TypeCondition Directives? SelectionSet
547+
* - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
515548
*
516549
* TypeCondition : NamedType
517550
*/
518551
parseFragmentDefinition(): FragmentDefinitionNode {
519552
const start = this._lexer.token;
520553
this.expectKeyword('fragment');
521-
// Legacy support for defining variables within fragments changes
522-
// the grammar of FragmentDefinition:
523-
// - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
524-
if (this._options.allowLegacyFragmentVariables === true) {
525-
return this.node<FragmentDefinitionNode>(start, {
526-
kind: Kind.FRAGMENT_DEFINITION,
527-
name: this.parseFragmentName(),
528-
variableDefinitions: this.parseVariableDefinitions(),
529-
typeCondition: (this.expectKeyword('on'), this.parseNamedType()),
530-
directives: this.parseDirectives(false),
531-
selectionSet: this.parseSelectionSet(),
532-
});
533-
}
534554
return this.node<FragmentDefinitionNode>(start, {
535555
kind: Kind.FRAGMENT_DEFINITION,
536556
name: this.parseFragmentName(),
557+
variableDefinitions:
558+
this._options.experimentalFragmentArguments === true || this._options.allowLegacyFragmentVariables === true
559+
? this.parseVariableDefinitions()
560+
: undefined,
537561
typeCondition: (this.expectKeyword('on'), this.parseNamedType()),
538562
directives: this.parseDirectives(false),
539563
selectionSet: this.parseSelectionSet(),
564+
540565
});
541566
}
542567

src/language/printer.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,8 @@ const printDocASTReducer: ASTReducer<string> = {
5757
Field: {
5858
leave({ alias, name, arguments: args, directives, selectionSet }) {
5959
const prefix = wrap('', alias, ': ') + name;
60-
let argsLine = prefix + wrap('(', join(args, ', '), ')');
6160

62-
if (argsLine.length > MAX_LINE_LENGTH) {
63-
argsLine = prefix + wrap('(\n', indent(join(args, '\n')), '\n)');
64-
}
65-
66-
return join([argsLine, join(directives, ' '), selectionSet], ' ');
61+
return join([wrappedLineAndArgs(prefix, args), join(directives, ' '), selectionSet], ' ');
6762
},
6863
},
6964

@@ -72,8 +67,12 @@ const printDocASTReducer: ASTReducer<string> = {
7267
// Fragments
7368

7469
FragmentSpread: {
75-
leave: ({ name, directives }) =>
76-
'...' + name + wrap(' ', join(directives, ' ')),
70+
leave: ({ name, arguments: args, directives }) => {
71+
const prefix = '...' + name;
72+
return (
73+
wrappedLineAndArgs(prefix, args) + wrap(' ', join(directives, ' '))
74+
);
75+
},
7776
},
7877

7978
InlineFragment: {
@@ -345,3 +344,15 @@ function hasMultilineItems(maybeArray: Maybe<ReadonlyArray<string>>): boolean {
345344
/* c8 ignore next */
346345
return maybeArray?.some((str) => str.includes('\n')) ?? false;
347346
}
347+
348+
function wrappedLineAndArgs(
349+
prefix: string,
350+
args: ReadonlyArray<string> | undefined,
351+
): string {
352+
let argsLine = prefix + wrap('(', join(args, ', '), ')');
353+
354+
if (argsLine.length > MAX_LINE_LENGTH) {
355+
argsLine = prefix + wrap('(\n', indent(join(args, '\n')), '\n)');
356+
}
357+
return argsLine;
358+
}

src/type/__tests__/introspection-test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,11 @@ describe('Introspection', () => {
859859
isDeprecated: false,
860860
deprecationReason: null,
861861
},
862+
{
863+
name: 'FRAGMENT_VARIABLE_DEFINITION',
864+
isDeprecated: false,
865+
deprecationReason: null,
866+
},
862867
{
863868
name: 'SCHEMA',
864869
isDeprecated: false,

src/type/introspection.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,11 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({
155155
},
156156
VARIABLE_DEFINITION: {
157157
value: DirectiveLocation.VARIABLE_DEFINITION,
158-
description: 'Location adjacent to a variable definition.',
158+
description: 'Location adjacent to an operation variable definition.',
159+
},
160+
FRAGMENT_VARIABLE_DEFINITION: {
161+
value: DirectiveLocation.FRAGMENT_VARIABLE_DEFINITION,
162+
description: 'Location adjacent to a fragment variable definition.',
159163
},
160164
SCHEMA: {
161165
value: DirectiveLocation.SCHEMA,

src/utilities/__tests__/printSchema-test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,9 +848,12 @@ describe('Type System Printer', () => {
848848
"""Location adjacent to an inline fragment."""
849849
INLINE_FRAGMENT
850850
851-
"""Location adjacent to a variable definition."""
851+
"""Location adjacent to an operation variable definition."""
852852
VARIABLE_DEFINITION
853853
854+
"""Location adjacent to a fragment variable definition."""
855+
FRAGMENT_VARIABLE_DEFINITION
856+
854857
"""Location adjacent to a schema definition."""
855858
SCHEMA
856859

0 commit comments

Comments
 (0)