Skip to content

Commit 95d34e6

Browse files
committed
Add descriptions to executable definitions
Implements graphql/graphql-spec#892
1 parent d26f6e9 commit 95d34e6

File tree

8 files changed

+115
-46
lines changed

8 files changed

+115
-46
lines changed

src/__testUtils__/kitchenSinkQuery.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
export const kitchenSinkQuery: string = String.raw`
2-
query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
2+
"Query description"
3+
query queryName(
4+
"Very complex variable"
5+
$foo: ComplexType,
6+
$site: Site = MOBILE,
7+
) @onQuery {
38
whoever123is: node(id: [123, 456]) {
49
id
510
... on User @onInlineFragment {
@@ -44,6 +49,9 @@ subscription StoryLikeSubscription(
4449
}
4550
}
4651
52+
"""
53+
Fragment description
54+
"""
4755
fragment frag on Friend @onFragmentDefinition {
4856
foo(
4957
size: $size

src/language/__tests__/parser-test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ describe('Parser', () => {
241241
{
242242
kind: Kind.OPERATION_DEFINITION,
243243
loc: { start: 0, end: 40 },
244+
description: undefined,
244245
operation: 'query',
245246
name: undefined,
246247
variableDefinitions: [],
@@ -317,6 +318,7 @@ describe('Parser', () => {
317318

318319
it('creates ast from nameless query without variables', () => {
319320
const result = parse(dedent`
321+
"Query description"
320322
query {
321323
node {
322324
id
@@ -326,41 +328,47 @@ describe('Parser', () => {
326328

327329
expectJSON(result).toDeepEqual({
328330
kind: Kind.DOCUMENT,
329-
loc: { start: 0, end: 29 },
331+
loc: { start: 0, end: 49 },
330332
definitions: [
331333
{
332334
kind: Kind.OPERATION_DEFINITION,
333-
loc: { start: 0, end: 29 },
335+
loc: { start: 0, end: 49 },
336+
description: {
337+
kind: Kind.STRING,
338+
loc: { start: 0, end: 19 },
339+
block: false,
340+
value: 'Query description',
341+
},
334342
operation: 'query',
335343
name: undefined,
336344
variableDefinitions: [],
337345
directives: [],
338346
selectionSet: {
339347
kind: Kind.SELECTION_SET,
340-
loc: { start: 6, end: 29 },
348+
loc: { start: 26, end: 49 },
341349
selections: [
342350
{
343351
kind: Kind.FIELD,
344-
loc: { start: 10, end: 27 },
352+
loc: { start: 30, end: 47 },
345353
alias: undefined,
346354
name: {
347355
kind: Kind.NAME,
348-
loc: { start: 10, end: 14 },
356+
loc: { start: 30, end: 34 },
349357
value: 'node',
350358
},
351359
arguments: [],
352360
directives: [],
353361
selectionSet: {
354362
kind: Kind.SELECTION_SET,
355-
loc: { start: 15, end: 27 },
363+
loc: { start: 35, end: 47 },
356364
selections: [
357365
{
358366
kind: Kind.FIELD,
359-
loc: { start: 21, end: 23 },
367+
loc: { start: 41, end: 43 },
360368
alias: undefined,
361369
name: {
362370
kind: Kind.NAME,
363-
loc: { start: 21, end: 23 },
371+
loc: { start: 41, end: 43 },
364372
value: 'id',
365373
},
366374
arguments: [],

src/language/__tests__/printer-test.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,21 @@ describe('Printer: Query document', () => {
4444
`);
4545

4646
const queryASTWithArtifacts = parse(
47-
'query ($foo: TestType) @testDirective { id, name }',
47+
'"Query description" query ($foo: TestType) @testDirective { id, name }',
4848
);
4949
expect(print(queryASTWithArtifacts)).to.equal(dedent`
50+
"Query description"
5051
query ($foo: TestType) @testDirective {
5152
id
5253
name
5354
}
5455
`);
5556

5657
const mutationASTWithArtifacts = parse(
57-
'mutation ($foo: TestType) @testDirective { id, name }',
58+
'"Mutation description" mutation ($foo: TestType) @testDirective { id, name }',
5859
);
5960
expect(print(mutationASTWithArtifacts)).to.equal(dedent`
61+
"Mutation description"
6062
mutation ($foo: TestType) @testDirective {
6163
id
6264
name
@@ -66,10 +68,13 @@ describe('Printer: Query document', () => {
6668

6769
it('prints query with variable directives', () => {
6870
const queryASTWithVariableDirective = parse(
69-
'query ($foo: TestType = {a: 123} @testDirective(if: true) @test) { id }',
71+
'query ("Variable description" $foo: TestType = {a: 123} @testDirective(if: true) @test) { id }',
7072
);
7173
expect(print(queryASTWithVariableDirective)).to.equal(dedent`
72-
query ($foo: TestType = {a: 123} @testDirective(if: true) @test) {
74+
query (
75+
"Variable description"
76+
$foo: TestType = {a: 123} @testDirective(if: true) @test
77+
) {
7378
id
7479
}
7580
`);
@@ -110,6 +115,19 @@ describe('Printer: Query document', () => {
110115
`);
111116
});
112117

118+
it('prints fragment', () => {
119+
const printed = print(
120+
parse('"Fragment description" fragment Foo on Bar { baz }'),
121+
);
122+
123+
expect(printed).to.equal(dedent`
124+
"Fragment description"
125+
fragment Foo on Bar {
126+
baz
127+
}
128+
`);
129+
});
130+
113131
it('Legacy: prints fragment with variable directives', () => {
114132
const queryASTWithVariableDirective = parse(
115133
'fragment Foo($foo: TestType @test) on TestType @testDirective { id }',
@@ -150,7 +168,12 @@ describe('Printer: Query document', () => {
150168

151169
expect(printed).to.equal(
152170
dedentString(String.raw`
153-
query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
171+
"Query description"
172+
query queryName(
173+
"Very complex variable"
174+
$foo: ComplexType
175+
$site: Site = MOBILE
176+
) @onQuery {
154177
whoever123is: node(id: [123, 456]) {
155178
id
156179
... on User @onInlineFragment {
@@ -192,6 +215,7 @@ describe('Printer: Query document', () => {
192215
}
193216
}
194217
218+
"""Fragment description"""
195219
fragment frag on Friend @onFragmentDefinition {
196220
foo(
197221
size: $size

src/language/__tests__/schema-parser-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ describe('Schema Parser', () => {
331331
}
332332
`).to.deep.equal({
333333
message:
334-
'Syntax Error: Unexpected description, descriptions are supported only on type definitions.',
334+
'Syntax Error: Unexpected description, descriptions are not supported on type extensions.',
335335
locations: [{ line: 2, column: 7 }],
336336
});
337337

@@ -353,7 +353,7 @@ describe('Schema Parser', () => {
353353
}
354354
`).to.deep.equal({
355355
message:
356-
'Syntax Error: Unexpected description, descriptions are supported only on type definitions.',
356+
'Syntax Error: Unexpected description, descriptions are not supported on type extensions.',
357357
locations: [{ line: 2, column: 7 }],
358358
});
359359

src/language/__tests__/visitor-test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,13 @@ describe('Visitor', () => {
539539
expect(visited).to.deep.equal([
540540
['enter', 'Document', undefined, undefined],
541541
['enter', 'OperationDefinition', 0, undefined],
542+
['enter', 'StringValue', 'description', 'OperationDefinition'],
543+
['leave', 'StringValue', 'description', 'OperationDefinition'],
542544
['enter', 'Name', 'name', 'OperationDefinition'],
543545
['leave', 'Name', 'name', 'OperationDefinition'],
544546
['enter', 'VariableDefinition', 0, undefined],
547+
['enter', 'StringValue', 'description', 'VariableDefinition'],
548+
['leave', 'StringValue', 'description', 'VariableDefinition'],
545549
['enter', 'Variable', 'variable', 'VariableDefinition'],
546550
['enter', 'Name', 'name', 'Variable'],
547551
['leave', 'Name', 'name', 'Variable'],
@@ -793,6 +797,8 @@ describe('Visitor', () => {
793797
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
794798
['leave', 'OperationDefinition', 2, undefined],
795799
['enter', 'FragmentDefinition', 3, undefined],
800+
['enter', 'StringValue', 'description', 'FragmentDefinition'],
801+
['leave', 'StringValue', 'description', 'FragmentDefinition'],
796802
['enter', 'Name', 'name', 'FragmentDefinition'],
797803
['leave', 'Name', 'name', 'FragmentDefinition'],
798804
['enter', 'NamedType', 'typeCondition', 'FragmentDefinition'],

src/language/ast.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,19 @@ export const QueryDocumentKeys: {
198198

199199
Document: ['definitions'],
200200
OperationDefinition: [
201+
'description',
201202
'name',
202203
'variableDefinitions',
203204
'directives',
204205
'selectionSet',
205206
],
206-
VariableDefinition: ['variable', 'type', 'defaultValue', 'directives'],
207+
VariableDefinition: [
208+
'description',
209+
'variable',
210+
'type',
211+
'defaultValue',
212+
'directives',
213+
],
207214
Variable: ['name'],
208215
SelectionSet: ['selections'],
209216
Field: ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
@@ -212,6 +219,7 @@ export const QueryDocumentKeys: {
212219
FragmentSpread: ['name', 'directives'],
213220
InlineFragment: ['typeCondition', 'directives', 'selectionSet'],
214221
FragmentDefinition: [
222+
'description',
215223
'name',
216224
// Note: fragment variable definitions are deprecated and will removed in v17.0.0
217225
'variableDefinitions',
@@ -316,6 +324,7 @@ export type ExecutableDefinitionNode =
316324
export interface OperationDefinitionNode {
317325
readonly kind: Kind.OPERATION_DEFINITION;
318326
readonly loc?: Location;
327+
readonly description?: StringValueNode;
319328
readonly operation: OperationTypeNode;
320329
readonly name?: NameNode;
321330
readonly variableDefinitions?: ReadonlyArray<VariableDefinitionNode>;
@@ -332,6 +341,7 @@ export enum OperationTypeNode {
332341
export interface VariableDefinitionNode {
333342
readonly kind: Kind.VARIABLE_DEFINITION;
334343
readonly loc?: Location;
344+
readonly description?: StringValueNode;
335345
readonly variable: VariableNode;
336346
readonly type: TypeNode;
337347
readonly defaultValue?: ConstValueNode;
@@ -396,6 +406,7 @@ export interface InlineFragmentNode {
396406
export interface FragmentDefinitionNode {
397407
readonly kind: Kind.FRAGMENT_DEFINITION;
398408
readonly loc?: Location;
409+
readonly description?: StringValueNode;
399410
readonly name: NameNode;
400411
/** @deprecated variableDefinitions will be removed in v17.0.0 */
401412
readonly variableDefinitions?: ReadonlyArray<VariableDefinitionNode>;

src/language/parser.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ export class Parser {
250250

251251
if (keywordToken.kind === TokenKind.NAME) {
252252
switch (keywordToken.value) {
253+
case 'query':
254+
case 'mutation':
255+
case 'subscription':
256+
return this.parseOperationDefinition();
257+
case 'fragment':
258+
return this.parseFragmentDefinition();
253259
case 'schema':
254260
return this.parseSchemaDefinition();
255261
case 'scalar':
@@ -266,24 +272,14 @@ export class Parser {
266272
return this.parseInputObjectTypeDefinition();
267273
case 'directive':
268274
return this.parseDirectiveDefinition();
269-
}
270-
271-
if (hasDescription) {
272-
throw syntaxError(
273-
this._lexer.source,
274-
this._lexer.token.start,
275-
'Unexpected description, descriptions are supported only on type definitions.',
276-
);
277-
}
278-
279-
switch (keywordToken.value) {
280-
case 'query':
281-
case 'mutation':
282-
case 'subscription':
283-
return this.parseOperationDefinition();
284-
case 'fragment':
285-
return this.parseFragmentDefinition();
286275
case 'extend':
276+
if (hasDescription) {
277+
throw syntaxError(
278+
this._lexer.source,
279+
this._lexer.token.start,
280+
'Unexpected description, descriptions are not supported on type extensions.',
281+
);
282+
}
287283
return this.parseTypeSystemExtension();
288284
}
289285
}
@@ -300,23 +296,28 @@ export class Parser {
300296
*/
301297
parseOperationDefinition(): OperationDefinitionNode {
302298
const start = this._lexer.token;
299+
303300
if (this.peek(TokenKind.BRACE_L)) {
304301
return this.node<OperationDefinitionNode>(start, {
305302
kind: Kind.OPERATION_DEFINITION,
303+
description: undefined,
306304
operation: OperationTypeNode.QUERY,
307305
name: undefined,
308306
variableDefinitions: [],
309307
directives: [],
310308
selectionSet: this.parseSelectionSet(),
311309
});
312310
}
311+
312+
const description = this.parseDescription();
313313
const operation = this.parseOperationType();
314314
let name;
315315
if (this.peek(TokenKind.NAME)) {
316316
name = this.parseName();
317317
}
318318
return this.node<OperationDefinitionNode>(start, {
319319
kind: Kind.OPERATION_DEFINITION,
320+
description,
320321
operation,
321322
name,
322323
variableDefinitions: this.parseVariableDefinitions(),
@@ -359,6 +360,7 @@ export class Parser {
359360
parseVariableDefinition(): VariableDefinitionNode {
360361
return this.node<VariableDefinitionNode>(this._lexer.token, {
361362
kind: Kind.VARIABLE_DEFINITION,
363+
description: this.parseDescription(),
362364
variable: this.parseVariable(),
363365
type: (this.expectToken(TokenKind.COLON), this.parseTypeReference()),
364366
defaultValue: this.expectOptionalToken(TokenKind.EQUALS)
@@ -506,13 +508,15 @@ export class Parser {
506508
*/
507509
parseFragmentDefinition(): FragmentDefinitionNode {
508510
const start = this._lexer.token;
511+
const description = this.parseDescription();
509512
this.expectKeyword('fragment');
510513
// Legacy support for defining variables within fragments changes
511514
// the grammar of FragmentDefinition:
512515
// - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
513516
if (this._options?.allowLegacyFragmentVariables === true) {
514517
return this.node<FragmentDefinitionNode>(start, {
515518
kind: Kind.FRAGMENT_DEFINITION,
519+
description,
516520
name: this.parseFragmentName(),
517521
variableDefinitions: this.parseVariableDefinitions(),
518522
typeCondition: (this.expectKeyword('on'), this.parseNamedType()),
@@ -522,6 +526,7 @@ export class Parser {
522526
}
523527
return this.node<FragmentDefinitionNode>(start, {
524528
kind: Kind.FRAGMENT_DEFINITION,
529+
description,
525530
name: this.parseFragmentName(),
526531
typeCondition: (this.expectKeyword('on'), this.parseNamedType()),
527532
directives: this.parseDirectives(false),

0 commit comments

Comments
 (0)