Skip to content

Commit 1e5b373

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. 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 3fa0624 commit 1e5b373

16 files changed

+808
-202
lines changed

src/jsutils/dedent.js

+2-11
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,7 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11-
/**
12-
* fixes identation by removing leading spaces from each line
13-
*/
14-
function fixIdent(str: string): string {
15-
const indent = /^\n?( *)/.exec(str)[1]; // figure out ident
16-
return str
17-
.replace(RegExp('^' + indent, 'mg'), '') // remove ident
18-
.replace(/^\n*/m, '') // remove leading newline
19-
.replace(/ *$/, ''); // remove trailing spaces
20-
}
11+
import trimIndentation from './trimIndentation';
2112

2213
/**
2314
* An ES6 string tag that fixes identation. Also removes leading newlines
@@ -46,5 +37,5 @@ export default function dedent(
4637
}
4738
}
4839

49-
return fixIdent(res);
40+
return trimIndentation(res) + '\n';
5041
}

src/jsutils/trimIndentation.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* @flow */
2+
/**
3+
* Copyright (c) 2017, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*/
10+
11+
/**
12+
* Removes leading identation from each line in a multi-line string.
13+
*
14+
* Note: this is similar to Python's docstring "trim" operation.
15+
* https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
16+
*/
17+
export default function trimIndentation(multiLine: string): string {
18+
// Expand a multi-line string into independent lines.
19+
const lines = multiLine.split('\n');
20+
21+
// Determine minimum indentation, not including the first line.
22+
let minIndent;
23+
for (let i = 1; i < lines.length; i++) {
24+
const line = lines[i];
25+
const lineIndent = leadingWhitespace(line);
26+
if (
27+
lineIndent < line.length &&
28+
(minIndent === undefined || lineIndent < minIndent)
29+
) {
30+
minIndent = lineIndent;
31+
if (minIndent === 0) {
32+
break;
33+
}
34+
}
35+
}
36+
37+
// Remove indentation, treating the first line specially.
38+
const firstIndent = leadingWhitespace(lines[0]);
39+
if (firstIndent) {
40+
lines[0] = lines[0].slice(firstIndent);
41+
}
42+
if (minIndent) {
43+
for (let i = 1; i < lines.length; i++) {
44+
lines[i] = lines[i].slice(minIndent);
45+
}
46+
}
47+
48+
// Remove trailing and leading empty lines.
49+
while (lines.length > 0 && lines[lines.length - 1].length === 0) {
50+
lines.pop();
51+
}
52+
while (lines.length > 0 && lines[0].length === 0) {
53+
lines.shift();
54+
}
55+
56+
// Return a multi-line string.
57+
return lines.join('\n');
58+
}
59+
60+
function leadingWhitespace(str) {
61+
let i = 0;
62+
for (; i < str.length; i++) {
63+
if (str[i] !== ' ' && str[i] !== '\t') {
64+
break;
65+
}
66+
}
67+
return i;
68+
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ schema {
1010
mutation: MutationType
1111
}
1212

13+
"""
14+
This is a description
15+
of the `Foo` type.
16+
"""
1317
type Foo implements Bar {
1418
one: Type
1519
two(argument: InputType!): Type

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

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

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

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

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

+4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ describe('Printer', () => {
5656
mutation: MutationType
5757
}
5858
59+
"""
60+
This is a description
61+
of the \`Foo\` type.
62+
"""
5963
type Foo implements Bar {
6064
one: Type
6165
two(argument: InputType!): Type

src/language/ast.js

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

405406
export type ObjectTypeDefinitionNode = {
406407
kind: 'ObjectTypeDefinition';
407408
loc?: Location;
409+
description?: ?StringValueNode;
408410
name: NameNode;
409411
interfaces?: ?Array<NamedTypeNode>;
410412
directives?: ?Array<DirectiveNode>;
@@ -414,6 +416,7 @@ export type ObjectTypeDefinitionNode = {
414416
export type FieldDefinitionNode = {
415417
kind: 'FieldDefinition';
416418
loc?: Location;
419+
description?: ?StringValueNode;
417420
name: NameNode;
418421
arguments: Array<InputValueDefinitionNode>;
419422
type: TypeNode;
@@ -423,6 +426,7 @@ export type FieldDefinitionNode = {
423426
export type InputValueDefinitionNode = {
424427
kind: 'InputValueDefinition';
425428
loc?: Location;
429+
description?: ?StringValueNode;
426430
name: NameNode;
427431
type: TypeNode;
428432
defaultValue?: ?ValueNode;
@@ -432,6 +436,7 @@ export type InputValueDefinitionNode = {
432436
export type InterfaceTypeDefinitionNode = {
433437
kind: 'InterfaceTypeDefinition';
434438
loc?: Location;
439+
description?: ?StringValueNode;
435440
name: NameNode;
436441
directives?: ?Array<DirectiveNode>;
437442
fields: Array<FieldDefinitionNode>;
@@ -440,6 +445,7 @@ export type InterfaceTypeDefinitionNode = {
440445
export type UnionTypeDefinitionNode = {
441446
kind: 'UnionTypeDefinition';
442447
loc?: Location;
448+
description?: ?StringValueNode;
443449
name: NameNode;
444450
directives?: ?Array<DirectiveNode>;
445451
types: Array<NamedTypeNode>;
@@ -448,6 +454,7 @@ export type UnionTypeDefinitionNode = {
448454
export type EnumTypeDefinitionNode = {
449455
kind: 'EnumTypeDefinition';
450456
loc?: Location;
457+
description?: ?StringValueNode;
451458
name: NameNode;
452459
directives?: ?Array<DirectiveNode>;
453460
values: Array<EnumValueDefinitionNode>;
@@ -456,13 +463,15 @@ export type EnumTypeDefinitionNode = {
456463
export type EnumValueDefinitionNode = {
457464
kind: 'EnumValueDefinition';
458465
loc?: Location;
466+
description?: ?StringValueNode;
459467
name: NameNode;
460468
directives?: ?Array<DirectiveNode>;
461469
};
462470

463471
export type InputObjectTypeDefinitionNode = {
464472
kind: 'InputObjectTypeDefinition';
465473
loc?: Location;
474+
description?: ?StringValueNode;
466475
name: NameNode;
467476
directives?: ?Array<DirectiveNode>;
468477
fields: Array<InputValueDefinitionNode>;
@@ -477,6 +486,7 @@ export type TypeExtensionDefinitionNode = {
477486
export type DirectiveDefinitionNode = {
478487
kind: 'DirectiveDefinition';
479488
loc?: Location;
489+
description?: ?StringValueNode;
480490
name: NameNode;
481491
arguments?: ?Array<InputValueDefinitionNode>;
482492
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)