Skip to content

Commit 7ff4fe1

Browse files
committed
RFC: Descriptions as strings.
As discussed in graphql/graphql-spec#90 This proposes replacing leading comment blocks as descriptions in the schema definition language with leading strings (typically block strings). While I think there is some reduced ergonomics of using a string literal instead of a comment to write descriptions (unless perhaps you are accustomed to Python or Clojure), there are some compelling advantages: * Descriptions are first-class in the AST of the schema definition language. * Comments can remain "ignored" characters. * No ambiguity between commented out regions and descriptions. Specific to this reference implementation, since this is a breaking change and comment descriptions in the experimental SDL have fairly wide usage, I've left the comment description implementation intact and allow it to be enabled via an option. This should help with allowing upgrading with minimal impact on existing codebases and aid in automated transforms.
1 parent 7242137 commit 7ff4fe1

14 files changed

+792
-195
lines changed

src/language/__tests__/schema-kitchen-sink.graphql

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ schema {
88
mutation: MutationType
99
}
1010

11+
"""
12+
This is a description
13+
of the `Foo` type.
14+
"""
1115
type Foo implements Bar {
1216
one: Type
1317
two(argument: InputType!): Type

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

+58
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,55 @@ type Hello {
9494
expect(printJson(doc)).to.equal(printJson(expected));
9595
});
9696

97+
it('parses type with description string', () => {
98+
const doc = parse(`
99+
"Description"
100+
type Hello {
101+
world: String
102+
}`);
103+
expect(doc).to.containSubset({
104+
kind: 'Document',
105+
definitions: [
106+
{
107+
kind: 'ObjectTypeDefinition',
108+
name: nameNode('Hello', { start: 20, end: 25 }),
109+
description: {
110+
kind: 'StringValue',
111+
value: 'Description',
112+
loc: { start: 1, end: 14 },
113+
}
114+
}
115+
],
116+
loc: { start: 0, end: 45 },
117+
});
118+
});
119+
120+
it('parses type with description multi-line string', () => {
121+
const doc = parse(`
122+
"""
123+
Description
124+
"""
125+
# Even with comments between them
126+
type Hello {
127+
world: String
128+
}`);
129+
expect(doc).to.containSubset({
130+
kind: 'Document',
131+
definitions: [
132+
{
133+
kind: 'ObjectTypeDefinition',
134+
name: nameNode('Hello', { start: 60, end: 65 }),
135+
description: {
136+
kind: 'StringValue',
137+
value: 'Description',
138+
loc: { start: 1, end: 20 },
139+
}
140+
}
141+
],
142+
loc: { start: 0, end: 85 },
143+
});
144+
});
145+
97146
it('Simple extension', () => {
98147
const body = `
99148
extend type Hello {
@@ -128,6 +177,15 @@ extend type Hello {
128177
expect(printJson(doc)).to.equal(printJson(expected));
129178
});
130179

180+
it('Extension do not include descriptions', () => {
181+
expect(() => parse(`
182+
"Description"
183+
extend type Hello {
184+
world: String
185+
}
186+
`)).to.throw('Syntax Error GraphQL request (2:7)');
187+
});
188+
131189
it('Simple non-null type', () => {
132190
const body = `
133191
type Hello {

src/language/__tests__/schema-printer-test.js

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ describe('Printer', () => {
5454
mutation: MutationType
5555
}
5656
57+
"""
58+
This is a description
59+
of the \`Foo\` type.
60+
"""
5761
type Foo implements Bar {
5862
one: Type
5963
two(argument: InputType!): Type

src/language/ast.js

+10
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,15 @@ export type TypeDefinitionNode =
397397
export type ScalarTypeDefinitionNode = {
398398
kind: 'ScalarTypeDefinition';
399399
loc?: Location;
400+
description?: ?StringValueNode;
400401
name: NameNode;
401402
directives?: ?Array<DirectiveNode>;
402403
};
403404

404405
export type ObjectTypeDefinitionNode = {
405406
kind: 'ObjectTypeDefinition';
406407
loc?: Location;
408+
description?: ?StringValueNode;
407409
name: NameNode;
408410
interfaces?: ?Array<NamedTypeNode>;
409411
directives?: ?Array<DirectiveNode>;
@@ -413,6 +415,7 @@ export type ObjectTypeDefinitionNode = {
413415
export type FieldDefinitionNode = {
414416
kind: 'FieldDefinition';
415417
loc?: Location;
418+
description?: ?StringValueNode;
416419
name: NameNode;
417420
arguments: Array<InputValueDefinitionNode>;
418421
type: TypeNode;
@@ -422,6 +425,7 @@ export type FieldDefinitionNode = {
422425
export type InputValueDefinitionNode = {
423426
kind: 'InputValueDefinition';
424427
loc?: Location;
428+
description?: ?StringValueNode;
425429
name: NameNode;
426430
type: TypeNode;
427431
defaultValue?: ?ValueNode;
@@ -431,6 +435,7 @@ export type InputValueDefinitionNode = {
431435
export type InterfaceTypeDefinitionNode = {
432436
kind: 'InterfaceTypeDefinition';
433437
loc?: Location;
438+
description?: ?StringValueNode;
434439
name: NameNode;
435440
directives?: ?Array<DirectiveNode>;
436441
fields: Array<FieldDefinitionNode>;
@@ -439,6 +444,7 @@ export type InterfaceTypeDefinitionNode = {
439444
export type UnionTypeDefinitionNode = {
440445
kind: 'UnionTypeDefinition';
441446
loc?: Location;
447+
description?: ?StringValueNode;
442448
name: NameNode;
443449
directives?: ?Array<DirectiveNode>;
444450
types: Array<NamedTypeNode>;
@@ -447,6 +453,7 @@ export type UnionTypeDefinitionNode = {
447453
export type EnumTypeDefinitionNode = {
448454
kind: 'EnumTypeDefinition';
449455
loc?: Location;
456+
description?: ?StringValueNode;
450457
name: NameNode;
451458
directives?: ?Array<DirectiveNode>;
452459
values: Array<EnumValueDefinitionNode>;
@@ -455,13 +462,15 @@ export type EnumTypeDefinitionNode = {
455462
export type EnumValueDefinitionNode = {
456463
kind: 'EnumValueDefinition';
457464
loc?: Location;
465+
description?: ?StringValueNode;
458466
name: NameNode;
459467
directives?: ?Array<DirectiveNode>;
460468
};
461469

462470
export type InputObjectTypeDefinitionNode = {
463471
kind: 'InputObjectTypeDefinition';
464472
loc?: Location;
473+
description?: ?StringValueNode;
465474
name: NameNode;
466475
directives?: ?Array<DirectiveNode>;
467476
fields: Array<InputValueDefinitionNode>;
@@ -476,6 +485,7 @@ export type TypeExtensionDefinitionNode = {
476485
export type DirectiveDefinitionNode = {
477486
kind: 'DirectiveDefinition';
478487
loc?: Location;
488+
description?: ?StringValueNode;
479489
name: NameNode;
480490
arguments?: ?Array<InputValueDefinitionNode>;
481491
locations: Array<NameNode>;

src/language/lexer.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,24 @@ export function createLexer<TOptions>(
3232
token: startOfFileToken,
3333
line: 1,
3434
lineStart: 0,
35-
advance: advanceLexer
35+
advance: advanceLexer,
36+
lookahead
3637
};
3738
return lexer;
3839
}
3940

4041
function advanceLexer() {
41-
let token = this.lastToken = this.token;
42+
this.lastToken = this.token;
43+
const token = this.token = this.lookahead();
44+
return token;
45+
}
46+
47+
function lookahead() {
48+
let token = this.token;
4249
if (token.kind !== EOF) {
4350
do {
44-
token = token.next = readToken(this, token);
51+
token = token.next || (token.next = readToken(this, token));
4552
} while (token.kind === COMMENT);
46-
this.token = token;
4753
}
4854
return token;
4955
}
@@ -79,6 +85,12 @@ export type Lexer<TOptions> = {
7985
* Advances the token stream to the next non-ignored token.
8086
*/
8187
advance(): Token;
88+
89+
/**
90+
* Looks ahead and returns the next non-ignored token, but does not change
91+
* the Lexer's state.
92+
*/
93+
lookahead(): Token;
8294
};
8395

8496
// Each kind of token.

0 commit comments

Comments
 (0)