diff --git a/.changeset/afraid-oranges-clean.md b/.changeset/afraid-oranges-clean.md new file mode 100644 index 00000000000..417922cf249 --- /dev/null +++ b/.changeset/afraid-oranges-clean.md @@ -0,0 +1,9 @@ +--- +'graphql-language-service': patch +--- + +fix: Correctly parse schema extensions with no root operations + +Previously, the parser gave schema extensions the same treatment as schema definitions. The requirements are slightly different, however, since a schema extension does not require a list of root operations according to the spec: https://spec.graphql.org/draft/#sec-Schema-Extension. + +The rule for parsing a schema extension is now distinct from that for a schema definition, allowing the root operations list to be omitted. diff --git a/packages/graphql-language-service/src/parser/Rules.ts b/packages/graphql-language-service/src/parser/Rules.ts index af74ca23823..59dcdaddfee 100644 --- a/packages/graphql-language-service/src/parser/Rules.ts +++ b/packages/graphql-language-service/src/parser/Rules.ts @@ -239,14 +239,8 @@ export const ParseRules: { [name: string]: ParseRule } = { 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: [ @@ -322,7 +316,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 5865c78202f..00c490f49cb 100644 --- a/packages/graphql-language-service/src/parser/__tests__/OnlineParser-test.ts +++ b/packages/graphql-language-service/src/parser/__tests__/OnlineParser-test.ts @@ -56,7 +56,7 @@ describe('onlineParser', () => { `); t.keyword('schema', { kind: 'SchemaDef' }); - t.punctuation('{'); + t.punctuation('{', { kind: 'OperationTypeDefs' }); t.keyword('query', { kind: 'OperationTypeDef' }); t.punctuation(':'); @@ -67,6 +67,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('{'); + 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(` {