diff --git a/.changeset/afraid-oranges-clean.md b/.changeset/afraid-oranges-clean.md new file mode 100644 index 0000000000..b093c2f172 --- /dev/null +++ b/.changeset/afraid-oranges-clean.md @@ -0,0 +1,17 @@ +--- +'graphql-language-service': patch +--- + +Align schema-language parser bodies with the GraphQL spec. + +The online parser previously required a body in several places where +the spec marks it as optional, causing valid schema documents to fail +to tokenize cleanly when a definition or extension omitted its body. +The following are now parsed correctly: + +- `extend schema` with no root operation type definitions +- `type` / `extend type` with no fields body +- `interface` / `extend interface` with no fields body +- `union` / `extend union` with no member list +- `enum` / `extend enum` with no values body +- `input` / `extend input` with no fields body diff --git a/packages/graphql-language-service/src/parser/Rules.ts b/packages/graphql-language-service/src/parser/Rules.ts index af74ca2382..210c8c67b9 100644 --- a/packages/graphql-language-service/src/parser/Rules.ts +++ b/packages/graphql-language-service/src/parser/Rules.ts @@ -232,21 +232,13 @@ export const ParseRules: { [name: string]: ParseRule } = { name('atom'), opt('Implements'), list('Directive'), - p('{'), - list('FieldDef'), - p('}'), + opt('FieldDefs'), ], Implements: [word('implements'), list('NamedType', p('&'))], DirectiveLocation: [name('string-2')], // GraphQL schema language - SchemaDef: [ - word('schema'), - list('Directive'), - p('{'), - list('OperationTypeDef'), - p('}'), - ], - + SchemaDef: [word('schema'), list('Directive'), 'OperationTypeDefs'], + OperationTypeDefs: [p('{'), list('OperationTypeDef'), p('}')], OperationTypeDef: [name('keyword'), p(':'), name('atom')], ScalarDef: [word('scalar'), name('atom'), list('Directive')], ObjectTypeDef: [ @@ -254,10 +246,9 @@ export const ParseRules: { [name: string]: ParseRule } = { name('atom'), opt('Implements'), list('Directive'), - p('{'), - list('FieldDef'), - p('}'), + opt('FieldDefs'), ], + FieldDefs: [p('{'), list('FieldDef'), p('}')], FieldDef: [ name('property'), @@ -280,29 +271,27 @@ export const ParseRules: { [name: string]: ParseRule } = { word('union'), name('atom'), list('Directive'), - p('='), - list('UnionMember', p('|')), + opt('UnionMembers'), ], + UnionMembers: [p('='), list('UnionMember', p('|'))], UnionMember: ['NamedType'], EnumDef: [ word('enum'), name('atom'), list('Directive'), - p('{'), - list('EnumValueDef'), - p('}'), + opt('EnumValueDefs'), ], + EnumValueDefs: [p('{'), list('EnumValueDef'), p('}')], EnumValueDef: [name('string-2'), list('Directive')], InputDef: [ word('input'), name('atom'), list('Directive'), - p('{'), - list('InputValueDef'), - p('}'), + opt('InputValueDefs'), ], + InputValueDefs: [p('{'), list('InputValueDef'), p('}')], ExtendDef: [word('extend'), 'ExtensionDefinition'], ExtensionDefinition(token: Token): RuleKind | void { switch (token.value) { @@ -322,7 +311,11 @@ export const ParseRules: { [name: string]: ParseRule } = { return Kind.INPUT_OBJECT_TYPE_EXTENSION; } }, - [Kind.SCHEMA_EXTENSION]: ['SchemaDef'], + [Kind.SCHEMA_EXTENSION]: [ + word('schema'), + list('Directive'), + opt('OperationTypeDefs'), + ], [Kind.SCALAR_TYPE_EXTENSION]: ['ScalarDef'], [Kind.OBJECT_TYPE_EXTENSION]: ['ObjectTypeDef'], [Kind.INTERFACE_TYPE_EXTENSION]: ['InterfaceDef'], diff --git a/packages/graphql-language-service/src/parser/__tests__/OnlineParser.test.ts b/packages/graphql-language-service/src/parser/__tests__/OnlineParser.test.ts index aec3609509..f594d80ee6 100644 --- a/packages/graphql-language-service/src/parser/__tests__/OnlineParser.test.ts +++ b/packages/graphql-language-service/src/parser/__tests__/OnlineParser.test.ts @@ -57,7 +57,7 @@ describe('onlineParser', () => { `); t.keyword('schema', { kind: 'SchemaDef' }); - t.punctuation('{'); + t.punctuation('{', { kind: 'OperationTypeDefs' }); t.keyword('query', { kind: 'OperationTypeDef' }); t.punctuation(':'); @@ -68,6 +68,71 @@ describe('onlineParser', () => { t.eol(); }); + it('parses schema extension bare', () => { + const { t } = getUtils(` + extend schema + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('schema', { kind: 'SchemaExtension' }); + + t.eol(); + }); + + it('parses schema extension with operation defs', () => { + const { t } = getUtils(` + extend schema { + query: SomeType + } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('schema', { kind: 'SchemaExtension' }); + t.punctuation('{', { kind: 'OperationTypeDefs' }); + + t.keyword('query', { kind: 'OperationTypeDef' }); + t.punctuation(':'); + t.name('SomeType'); + + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + + it('parses schema extension with directive applications', () => { + const { t } = getUtils(` + extend schema @someDirective + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('schema', { kind: 'SchemaExtension' }); + expectDirective({ t }, { name: 'someDirective' }); + + t.eol(); + }); + + it('parses schema extension with directive applications without root operation definitions, followed by a type definition', () => { + const { t } = getUtils(` + extend schema @someDirective + + type A { field: String } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('schema', { kind: 'SchemaExtension' }); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('A'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + it('parses short query', () => { const { t } = getUtils(` { @@ -908,7 +973,7 @@ describe('onlineParser', () => { `); t.keyword('type', { kind: 'ObjectTypeDef' }); t.name('SomeType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'FieldDefs' }); t.property('someField', { kind: 'FieldDef' }); t.punctuation(':'); @@ -920,6 +985,28 @@ describe('onlineParser', () => { t.eol(); }); + it('with no fields body, followed by another definition', () => { + const { t } = getUtils(` + type SomeType @someDirective + + type AnotherType { field: String } + `); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('SomeType'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + it('with an object implementing an interface', () => { const { t } = getUtils('type SomeType implements SomeInterface'); @@ -987,7 +1074,7 @@ describe('onlineParser', () => { `); t.keyword('interface', { kind: 'InterfaceDef' }); t.name('SomeInterface'); - t.punctuation('{'); + t.punctuation('{', { kind: 'FieldDefs' }); t.property('someField', { kind: 'FieldDef' }); t.punctuation(':'); @@ -999,6 +1086,28 @@ describe('onlineParser', () => { t.eol(); }); + it('with no fields body, followed by another definition', () => { + const { t } = getUtils(` + interface SomeInterface @someDirective + + type AnotherType { field: String } + `); + + t.keyword('interface', { kind: 'InterfaceDef' }); + t.name('SomeInterface'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + it('with a directive', () => { const { t } = getUtils('interface SomeInterface @someDirective'); @@ -1046,6 +1155,52 @@ describe('onlineParser', () => { ); }); + describe('parses extend interface def', () => { + it('correctly', () => { + const { t } = getUtils(` + extend interface SomeInterface { + someField: SomeType + } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('interface', { kind: 'InterfaceDef' }); + t.name('SomeInterface'); + t.punctuation('{', { kind: 'FieldDefs' }); + + t.property('someField', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('SomeType', { kind: 'NamedType' }); + + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + + it('with no fields body, only a directive', () => { + const { t } = getUtils(` + extend interface SomeInterface @someDirective + + type AnotherType { field: String } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('interface', { kind: 'InterfaceDef' }); + t.name('SomeInterface'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + }); + describe('parses field defs', () => { it('correctly', () => { const { t } = getUtils(` @@ -1055,7 +1210,7 @@ describe('onlineParser', () => { `); t.keyword('type', { kind: 'ObjectTypeDef' }); t.name('SomeType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'FieldDefs' }); t.property('someField', { kind: 'FieldDef' }); t.punctuation(':'); @@ -1075,7 +1230,7 @@ describe('onlineParser', () => { `); t.keyword('type', { kind: 'ObjectTypeDef' }); t.name('SomeType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'FieldDefs' }); t.property('someField', { kind: 'FieldDef' }); t.punctuation(/\(/, { kind: 'ArgumentsDef' }); @@ -1104,7 +1259,7 @@ describe('onlineParser', () => { t.keyword('type', { kind: 'ObjectTypeDef' }); t.name('SomeType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'FieldDefs' }); t.property('someField', { kind: 'FieldDef' }); t.punctuation(':'); @@ -1125,7 +1280,7 @@ describe('onlineParser', () => { t.keyword('type', { kind: 'ObjectTypeDef' }); t.name('SomeType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'FieldDefs' }); t.property('someField', { kind: 'FieldDef' }); t.punctuation(':'); @@ -1148,7 +1303,7 @@ describe('onlineParser', () => { it(`with a directive having arguments of type ${fill.type}`, () => { t.keyword('type', { kind: 'ObjectTypeDef' }); t.name('SomeType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'FieldDefs' }); t.property('someField', { kind: 'FieldDef' }); t.punctuation(':'); @@ -1181,7 +1336,7 @@ describe('onlineParser', () => { t.keyword('extend', { kind: 'ExtendDef' }); t.keyword('type', { kind: 'ObjectTypeDef' }); t.name('SomeType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'FieldDefs' }); t.property('someField', { kind: 'FieldDef' }); t.punctuation(':'); @@ -1202,7 +1357,7 @@ describe('onlineParser', () => { t.keyword('extend', { kind: 'ExtendDef' }); t.keyword('type', { kind: 'ObjectTypeDef' }); t.name('SomeType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'FieldDefs' }); t.property('someField', { kind: 'FieldDef' }); t.punctuation(':'); @@ -1214,6 +1369,29 @@ describe('onlineParser', () => { t.eol(); }); + + it('with no fields body, followed by another definition', () => { + const { t } = getUtils(` + extend type SomeType @someDirective + + type AnotherType { field: String } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('SomeType'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); }); describe('parses input type def', () => { @@ -1226,7 +1404,7 @@ describe('onlineParser', () => { t.keyword('input', { kind: 'InputDef' }); t.name('SomeInputType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'InputValueDefs' }); t.attribute('someField', { kind: 'InputValueDef' }); t.punctuation(':'); @@ -1246,7 +1424,7 @@ describe('onlineParser', () => { t.keyword('input', { kind: 'InputDef' }); t.name('SomeInputType'); - t.punctuation('{'); + t.punctuation('{', { kind: 'InputValueDefs' }); t.attribute('someField', { kind: 'InputValueDef' }); t.punctuation(':'); @@ -1258,6 +1436,74 @@ describe('onlineParser', () => { t.eol(); }); + + it('with no fields body, only a directive', () => { + const { t } = getUtils(` + input SomeInputType @someDirective + + type AnotherType { field: String } + `); + + t.keyword('input', { kind: 'InputDef' }); + t.name('SomeInputType'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + }); + + describe('parses extend input def', () => { + it('correctly', () => { + const { t } = getUtils(` + extend input SomeInputType { + someField: AnotherType + } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('input', { kind: 'InputDef' }); + t.name('SomeInputType'); + t.punctuation('{', { kind: 'InputValueDefs' }); + + t.attribute('someField', { kind: 'InputValueDef' }); + t.punctuation(':'); + t.name('AnotherType', { kind: 'NamedType' }); + + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + + it('with no fields body, only a directive', () => { + const { t } = getUtils(` + extend input SomeInputType @someDirective + + type AnotherType { field: String } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('input', { kind: 'InputDef' }); + t.name('SomeInputType'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); }); describe('parses enum type def', () => { @@ -1271,7 +1517,7 @@ describe('onlineParser', () => { t.keyword('enum', { kind: 'EnumDef' }); t.name('SomeEnum'); - t.punctuation('{'); + t.punctuation('{', { kind: 'EnumValueDefs' }); t.value('Enum', 'SOME_ENUM_VALUE', { kind: 'EnumValueDef' }); t.value('Enum', 'ANOTHER_ENUM_VALUE', { kind: 'EnumValueDef' }); @@ -1292,7 +1538,7 @@ describe('onlineParser', () => { t.keyword('enum', { kind: 'EnumDef' }); t.name('SomeEnum'); expectDirective({ t }, { name: 'someDirective' }); - t.punctuation('{', { kind: 'EnumDef' }); + t.punctuation('{', { kind: 'EnumValueDefs' }); t.value('Enum', 'SOME_ENUM_VALUE', { kind: 'EnumValueDef' }); t.value('Enum', 'ANOTHER_ENUM_VALUE', { kind: 'EnumValueDef' }); @@ -1301,6 +1547,72 @@ describe('onlineParser', () => { t.eol(); }); + + it('with no values body, only a directive', () => { + const { t } = getUtils(` + enum SomeEnum @someDirective + + type AnotherType { field: String } + `); + + t.keyword('enum', { kind: 'EnumDef' }); + t.name('SomeEnum'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + }); + + describe('parses extend enum def', () => { + it('correctly', () => { + const { t } = getUtils(` + extend enum SomeEnum { + SOME_ENUM_VALUE + } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('enum', { kind: 'EnumDef' }); + t.name('SomeEnum'); + t.punctuation('{', { kind: 'EnumValueDefs' }); + + t.value('Enum', 'SOME_ENUM_VALUE', { kind: 'EnumValueDef' }); + + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + + it('with no values body, only a directive', () => { + const { t } = getUtils(` + extend enum SomeEnum @someDirective + + type AnotherType { field: String } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('enum', { kind: 'EnumDef' }); + t.name('SomeEnum'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); }); describe('parses scalar type def', () => { @@ -1330,9 +1642,9 @@ describe('onlineParser', () => { t.keyword('union', { kind: 'UnionDef' }); t.name('SomeUnionType'); - t.punctuation('='); + t.punctuation('=', { kind: 'UnionMembers' }); t.name('SomeType', { kind: 'NamedType' }); - t.punctuation('|', { kind: 'UnionDef' }); + t.punctuation('|', { kind: 'UnionMembers' }); t.name('AnotherType', { kind: 'NamedType' }); t.eol(); @@ -1346,13 +1658,76 @@ describe('onlineParser', () => { t.keyword('union', { kind: 'UnionDef' }); t.name('SomeUnionType'); expectDirective({ t }, { name: 'someDirective' }); - t.punctuation('=', { kind: 'UnionDef' }); + t.punctuation('=', { kind: 'UnionMembers' }); + t.name('SomeType', { kind: 'NamedType' }); + t.punctuation('|', { kind: 'UnionMembers' }); + t.name('AnotherType', { kind: 'NamedType' }); + + t.eol(); + }); + + it('with no members, only a directive', () => { + const { t } = getUtils(` + union SomeUnionType @someDirective + + type AnotherType { field: String } + `); + + t.keyword('union', { kind: 'UnionDef' }); + t.name('SomeUnionType'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + }); + + describe('parses extend union def', () => { + it('correctly', () => { + const { t } = getUtils( + 'extend union SomeUnionType = SomeType | AnotherType', + ); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('union', { kind: 'UnionDef' }); + t.name('SomeUnionType'); + t.punctuation('=', { kind: 'UnionMembers' }); t.name('SomeType', { kind: 'NamedType' }); - t.punctuation('|', { kind: 'UnionDef' }); + t.punctuation('|', { kind: 'UnionMembers' }); t.name('AnotherType', { kind: 'NamedType' }); t.eol(); }); + + it('with no members, only a directive', () => { + const { t } = getUtils(` + extend union SomeUnionType @someDirective + + type AnotherType { field: String } + `); + + t.keyword('extend', { kind: 'ExtendDef' }); + t.keyword('union', { kind: 'UnionDef' }); + t.name('SomeUnionType'); + expectDirective({ t }, { name: 'someDirective' }); + + t.keyword('type', { kind: 'ObjectTypeDef' }); + t.name('AnotherType'); + t.punctuation('{', { kind: 'FieldDefs' }); + t.property('field', { kind: 'FieldDef' }); + t.punctuation(':'); + t.name('String', { kind: 'NamedType' }); + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); }); describe('parses directive type def', () => {