Skip to content

Add star syntax proposal #4344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 7 additions & 50 deletions src/language/__tests__/parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,63 +658,20 @@ describe('Parser', () => {
});
});

describe('parseDocumentDirective', () => {
it("doesn't throw on document-level directive", () => {
parse(dedent`
@SemanticNullability
type Query {
hello: String
world: String?
foo: String!
}
`);
});

it('parses semantic-non-null types', () => {
const result = parseType('MyType', { allowSemanticNullability: true });
expectJSON(result).toDeepEqual({
kind: Kind.SEMANTIC_NON_NULL_TYPE,
loc: { start: 0, end: 6 },
type: {
kind: Kind.NAMED_TYPE,
loc: { start: 0, end: 6 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
},
});
});

it('parses nullable types', () => {
const result = parseType('MyType?', { allowSemanticNullability: true });
expectJSON(result).toDeepEqual({
it('parses semantic-non-null types', () => {
const result = parseType('MyType*');
expectJSON(result).toDeepEqual({
kind: Kind.SEMANTIC_NON_NULL_TYPE,
loc: { start: 0, end: 7 },
type: {
kind: Kind.NAMED_TYPE,
loc: { start: 0, end: 6 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
});
});

it('parses non-nullable types', () => {
const result = parseType('MyType!', { allowSemanticNullability: true });
expectJSON(result).toDeepEqual({
kind: Kind.NON_NULL_TYPE,
loc: { start: 0, end: 7 },
type: {
kind: Kind.NAMED_TYPE,
loc: { start: 0, end: 6 },
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
},
});
},
});
});
});
31 changes: 5 additions & 26 deletions src/language/__tests__/schema-printer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,39 +182,18 @@ describe('Printer: SDL document', () => {
});

it('prints NamedType', () => {
expect(
print(parseType('MyType', { allowSemanticNullability: false }), {
useSemanticNullability: false,
}),
).to.equal(dedent`MyType`);
expect(print(parseType('MyType'))).to.equal(dedent`MyType`);
});

it('prints SemanticNullableType', () => {
expect(
print(parseType('MyType?', { allowSemanticNullability: true }), {
useSemanticNullability: true,
}),
).to.equal(dedent`MyType?`);
it('prints nullable types', () => {
expect(print(parseType('MyType'))).to.equal(dedent`MyType`);
});

it('prints SemanticNonNullType', () => {
expect(
print(parseType('MyType', { allowSemanticNullability: true }), {
useSemanticNullability: true,
}),
).to.equal(dedent`MyType`);
expect(print(parseType('MyType*'))).to.equal(dedent`MyType*`);
});

it('prints NonNullType', () => {
expect(
print(parseType('MyType!', { allowSemanticNullability: true }), {
useSemanticNullability: true,
}),
).to.equal(dedent`MyType!`);
expect(
print(parseType('MyType!', { allowSemanticNullability: false }), {
useSemanticNullability: true,
}),
).to.equal(dedent`MyType!`);
expect(print(parseType('MyType!'))).to.equal(dedent`MyType!`);
});
});
13 changes: 4 additions & 9 deletions src/language/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class Lexer {
export function isPunctuatorTokenKind(kind: TokenKind): boolean {
return (
kind === TokenKind.BANG ||
kind === TokenKind.QUESTION_MARK ||
kind === TokenKind.STAR ||
kind === TokenKind.DOLLAR ||
kind === TokenKind.AMP ||
kind === TokenKind.PAREN_L ||
Expand Down Expand Up @@ -247,16 +247,11 @@ function readNextToken(lexer: Lexer, start: number): Token {
// - FloatValue
// - StringValue
//
// Punctuator :: one of ! ? $ & ( ) ... : = @ [ ] { | }
// Punctuator :: one of ! * $ & ( ) ... : = @ [ ] { | }
case 0x0021: // !
return createToken(lexer, TokenKind.BANG, position, position + 1);
case 0x003f: // ?
return createToken(
lexer,
TokenKind.QUESTION_MARK,
position,
position + 1,
);
case 0x002a: // *
return createToken(lexer, TokenKind.STAR, position, position + 1);
case 0x0024: // $
return createToken(lexer, TokenKind.DOLLAR, position, position + 1);
case 0x0026: // &
Expand Down
60 changes: 16 additions & 44 deletions src/language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,6 @@ export interface ParseOptions {
* ```
*/
allowLegacyFragmentVariables?: boolean;

/**
* When enabled, the parser will understand and parse semantic nullability
* annotations. This means that every type suffixed with `!` will remain
* non-nullable, every type suffixed with `?` will be the classic nullable, and
* types without a suffix will be semantically nullable. Semantic nullability
* will be the new default when this is enabled. A semantically nullable type
* can only be null when there's an error associated with the field.
*
* @experimental
*/
allowSemanticNullability?: boolean;
}

/**
Expand Down Expand Up @@ -187,7 +175,7 @@ export function parseType(
): TypeNode | SemanticNonNullTypeNode {
const parser = new Parser(source, options);
parser.expectToken(TokenKind.SOF);
const type = parser.parseTypeReference();
const type = parser.parseTypeReference(true);
parser.expectToken(TokenKind.EOF);
return type;
}
Expand Down Expand Up @@ -271,16 +259,6 @@ export class Parser {
* - InputObjectTypeDefinition
*/
parseDefinition(): DefinitionNode {
const directives = this.parseDirectives(false);
// If a document-level SemanticNullability directive exists as
// the first element in a document, then all parsing will
// happen in SemanticNullability mode.
for (const directive of directives) {
if (directive.name.value === 'SemanticNullability') {
this._options.allowSemanticNullability = true;
}
}

if (this.peek(TokenKind.BRACE_L)) {
return this.parseOperationDefinition();
}
Expand Down Expand Up @@ -404,7 +382,7 @@ export class Parser {
kind: Kind.VARIABLE_DEFINITION,
variable: this.parseVariable(),
type: (this.expectToken(TokenKind.COLON),
this.parseTypeReference()) as TypeNode,
this.parseTypeReference(false)) as TypeNode,
defaultValue: this.expectOptionalToken(TokenKind.EQUALS)
? this.parseConstValueLiteral()
: undefined,
Expand Down Expand Up @@ -774,11 +752,13 @@ export class Parser {
* - ListType
* - NonNullType
*/
parseTypeReference(): TypeNode | SemanticNonNullTypeNode {
parseTypeReference(
allowSemanticNonNull: boolean,
): TypeNode | SemanticNonNullTypeNode {
const start = this._lexer.token;
let type;
if (this.expectOptionalToken(TokenKind.BRACKET_L)) {
const innerType = this.parseTypeReference();
const innerType = this.parseTypeReference(allowSemanticNonNull);
this.expectToken(TokenKind.BRACKET_R);
type = this.node<ListTypeNode>(start, {
kind: Kind.LIST_TYPE,
Expand All @@ -788,27 +768,19 @@ export class Parser {
type = this.parseNamedType();
}

if (this._options.allowSemanticNullability) {
if (this.expectOptionalToken(TokenKind.BANG)) {
return this.node<NonNullTypeNode>(start, {
kind: Kind.NON_NULL_TYPE,
type,
});
} else if (this.expectOptionalToken(TokenKind.QUESTION_MARK)) {
return type;
}

return this.node<SemanticNonNullTypeNode>(start, {
kind: Kind.SEMANTIC_NON_NULL_TYPE,
type,
});
}

if (this.expectOptionalToken(TokenKind.BANG)) {
return this.node<NonNullTypeNode>(start, {
kind: Kind.NON_NULL_TYPE,
type,
});
} else if (
allowSemanticNonNull &&
this.expectOptionalToken(TokenKind.STAR)
) {
return this.node<SemanticNonNullTypeNode>(start, {
kind: Kind.SEMANTIC_NON_NULL_TYPE,
type,
});
}

return type;
Expand Down Expand Up @@ -951,7 +923,7 @@ export class Parser {
const name = this.parseName();
const args = this.parseArgumentDefs();
this.expectToken(TokenKind.COLON);
const type = this.parseTypeReference();
const type = this.parseTypeReference(true);
const directives = this.parseConstDirectives();
return this.node<FieldDefinitionNode>(start, {
kind: Kind.FIELD_DEFINITION,
Expand Down Expand Up @@ -983,7 +955,7 @@ export class Parser {
const description = this.parseDescription();
const name = this.parseName();
this.expectToken(TokenKind.COLON);
const type = this.parseTypeReference();
const type = this.parseTypeReference(false);
let defaultValue;
if (this.expectOptionalToken(TokenKind.EQUALS)) {
defaultValue = this.parseConstValueLiteral();
Expand Down
22 changes: 3 additions & 19 deletions src/language/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,14 @@ import type { Maybe } from '../jsutils/Maybe';

import type { ASTNode } from './ast';
import { printBlockString } from './blockString';
import { Kind } from './kinds';
import { printString } from './printString';
import { visit } from './visitor';

/**
* Configuration options to control parser behavior
*/
export interface PrintOptions {
useSemanticNullability?: boolean;
}

/**
* Converts an AST into a string, using one set of reasonable
* formatting rules.
*/
export function print(ast: ASTNode, options: PrintOptions = {}): string {
export function print(ast: ASTNode): string {
return visit<string>(ast, {
Name: { leave: (node) => node.value },
Variable: { leave: (node) => '$' + node.name },
Expand Down Expand Up @@ -131,19 +123,11 @@ export function print(ast: ASTNode, options: PrintOptions = {}): string {
// Type

NamedType: {
leave: ({ name }, _, parent) =>
parent &&
!Array.isArray(parent) &&
((parent as ASTNode).kind === Kind.SEMANTIC_NON_NULL_TYPE ||
(parent as ASTNode).kind === Kind.NON_NULL_TYPE)
? name
: options?.useSemanticNullability
? `${name}?`
: name,
leave: ({ name }) => name,
},
ListType: { leave: ({ type }) => '[' + type + ']' },
NonNullType: { leave: ({ type }) => type + '!' },
SemanticNonNullType: { leave: ({ type }) => type },
SemanticNonNullType: { leave: ({ type }) => type + '*' },

// Type System Definitions

Expand Down
2 changes: 1 addition & 1 deletion src/language/tokenKind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ enum TokenKind {
SOF = '<SOF>',
EOF = '<EOF>',
BANG = '!',
QUESTION_MARK = '?',
STAR = '*',
DOLLAR = '$',
AMP = '&',
PAREN_L = '(',
Expand Down
10 changes: 4 additions & 6 deletions src/type/__tests__/introspection-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1798,11 +1798,10 @@ describe('Introspection', () => {
describe('semantic nullability', () => {
it('casts semantic-non-null types to nullable types in traditional mode', () => {
const schema = buildSchema(`
@SemanticNullability
type Query {
someField: String!
someField2: String
someField3: String?
someField2: String*
someField3: String
}
`);

Expand Down Expand Up @@ -1847,11 +1846,10 @@ describe('Introspection', () => {

it('returns semantic-non-null types in full mode', () => {
const schema = buildSchema(`
@SemanticNullability
type Query {
someField: String!
someField2: String
someField3: String?
someField2: String*
someField3: String
}
`);

Expand Down
2 changes: 1 addition & 1 deletion src/type/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ export class GraphQLSemanticNonNull<T extends GraphQLNullableType> {
}

toString(): string {
return String(this.ofType);
return String(this.ofType) + '*';
}

toJSON(): string {
Expand Down
13 changes: 6 additions & 7 deletions src/utilities/__tests__/TypeInfo-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,11 +460,10 @@ describe('visitWithTypeInfo', () => {

it('supports traversals of semantic non-null types', () => {
const schema = buildSchema(`
@SemanticNullability
type Query {
id: String!
name: String
something: String?
name: String*
something: String
}
`);

Expand Down Expand Up @@ -506,10 +505,10 @@ describe('visitWithTypeInfo', () => {
['enter', 'Name', 'id', 'String!'],
['leave', 'Name', 'id', 'String!'],
['leave', 'Field', null, 'String!'],
['enter', 'Field', null, 'String'],
['enter', 'Name', 'name', 'String'],
['leave', 'Name', 'name', 'String'],
['leave', 'Field', null, 'String'],
['enter', 'Field', null, 'String*'],
['enter', 'Name', 'name', 'String*'],
['leave', 'Name', 'name', 'String*'],
['leave', 'Field', null, 'String*'],
['enter', 'Field', null, 'String'],
['enter', 'Name', 'something', 'String'],
['leave', 'Name', 'something', 'String'],
Expand Down
Loading
Loading