Skip to content

Commit

Permalink
Add star syntax proposal
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Feb 15, 2025
1 parent 855e4d7 commit 2611aa4
Show file tree
Hide file tree
Showing 15 changed files with 92 additions and 217 deletions.
13 changes: 6 additions & 7 deletions src/language/__tests__/parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,20 +661,19 @@ describe('Parser', () => {
describe('parseDocumentDirective', () => {
it("doesn't throw on document-level directive", () => {
parse(dedent`
@SemanticNullability
type Query {
hello: String
world: String?
hello: String*
world: String
foo: String!
}
`);
});

it('parses semantic-non-null types', () => {
const result = parseType('MyType', { allowSemanticNullability: true });
const result = parseType('MyType*');
expectJSON(result).toDeepEqual({
kind: Kind.SEMANTIC_NON_NULL_TYPE,
loc: { start: 0, end: 6 },
loc: { start: 0, end: 7 },
type: {
kind: Kind.NAMED_TYPE,
loc: { start: 0, end: 6 },
Expand All @@ -688,7 +687,7 @@ describe('Parser', () => {
});

it('parses nullable types', () => {
const result = parseType('MyType?', { allowSemanticNullability: true });
const result = parseType('MyType');
expectJSON(result).toDeepEqual({
kind: Kind.NAMED_TYPE,
loc: { start: 0, end: 6 },
Expand All @@ -701,7 +700,7 @@ describe('Parser', () => {
});

it('parses non-nullable types', () => {
const result = parseType('MyType!', { allowSemanticNullability: true });
const result = parseType('MyType!');
expectJSON(result).toDeepEqual({
kind: Kind.NON_NULL_TYPE,
loc: { start: 0, end: 7 },
Expand Down
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
43 changes: 5 additions & 38 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 @@ -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 @@ -788,27 +766,16 @@ 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 (this.expectOptionalToken(TokenKind.STAR)) {
return this.node<SemanticNonNullTypeNode>(start, {
kind: Kind.SEMANTIC_NON_NULL_TYPE,
type,
});
}

return type;
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
10 changes: 3 additions & 7 deletions src/utilities/__tests__/buildClientSchema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -988,11 +988,9 @@ describe('Type System: build schema from introspection', () => {
describe('SemanticNullability', () => {
it('should build a client schema with semantic-non-null types', () => {
const sdl = dedent`
@SemanticNullability
type Query {
foo: String
bar: String?
foo: String*
bar: String
}
`;
const schema = buildSchema(sdl, { assumeValid: true });
Expand Down Expand Up @@ -1027,10 +1025,8 @@ describe('Type System: build schema from introspection', () => {

it('should throw when semantic-non-null types are too deep', () => {
const sdl = dedent`
@SemanticNullability
type Query {
bar: [[[[[[String?]]]]]]?
bar: [[[[[[String]*]*]*]*]*]
}
`;
const schema = buildSchema(sdl, { assumeValid: true });
Expand Down
14 changes: 6 additions & 8 deletions src/utilities/__tests__/extendSchema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,14 @@ describe('extendSchema', () => {

it('extends objects by adding new fields in semantic nullability mode', () => {
const schema = buildSchema(`
@SemanticNullability
type Query {
someObject: String
someObject: String*
}
`);
const extensionSDL = dedent`
@SemanticNullability
extend type Query {
newSemanticNonNullField: String
newSemanticNullableField: String?
newSemanticNonNullField: String*
newSemanticNullableField: String
newNonNullField: String!
}
`;
Expand All @@ -111,9 +109,9 @@ describe('extendSchema', () => {
expect(validateSchema(extendedSchema)).to.deep.equal([]);
expectSchemaChanges(schema, extendedSchema, true).to.equal(dedent`
type Query {
someObject: String
newSemanticNonNullField: String
newSemanticNullableField: String?
someObject: String*
newSemanticNonNullField: String*
newSemanticNullableField: String
newNonNullField: String!
}
`);
Expand Down
Loading

0 comments on commit 2611aa4

Please sign in to comment.