diff --git a/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts b/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts index e784200f9b4..54ef7b17eeb 100644 --- a/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts +++ b/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts @@ -397,6 +397,7 @@ describe('getAutocompleteSuggestions', () => { }, ]); }); + const metaArgs = [ { label: '__DirectiveLocation', @@ -409,6 +410,7 @@ describe('getAutocompleteSuggestions', () => { 'An enum describing what kind of type a given `__Type` is.', }, ]; + it('provides correct input type suggestions', () => { const result = testSuggestions( 'query($exampleVariable: ) { ', @@ -423,6 +425,45 @@ describe('getAutocompleteSuggestions', () => { { label: 'String', documentation: GraphQLString.description }, ]); }); + + it('provides correct input type suggestions for fragments', () => { + const result = testSuggestions( + 'fragment someFragment($exampleVariable: ) on Type { ', + new Position(0, 41), + ); + expect(result).toEqual([ + ...metaArgs, + { label: 'Boolean', documentation: GraphQLBoolean.description }, + { label: 'Episode' }, + { label: 'InputType' }, + { label: 'Int', documentation: GraphQLInt.description }, + { label: 'String', documentation: GraphQLString.description }, + ]); + }); + + it.skip('provides correct argument suggestions for fragment-spreads', () => { + const externalFragments = parse(` + fragment CharacterDetails($someVariable: Int) on Human { + name + } + `, {experimentalFragmentVariables:true}).definitions as FragmentDefinitionNode[]; + + const result = testSuggestions( + 'query { human(id: "1") { ...CharacterDetails() }}', + new Position(0, 46), + externalFragments, + ); + + expect(result).toEqual([ + { + label: 'someVariable', + insertText: 'someVariable: ', + command: suggestionCommand, + insertTextFormat: 2, + labelDetails: { detail: ' Int' }, + }, + ]); + }); it('provides filtered input type suggestions', () => { const result = testSuggestions( diff --git a/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts b/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts index dfc055cba8b..3f2bc63b17e 100644 --- a/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts +++ b/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts @@ -422,6 +422,7 @@ export function getAutocompleteSuggestions( if ( (kind === RuleKinds.VARIABLE_DEFINITION && step === 2) || (kind === RuleKinds.LIST_TYPE && step === 1) || + (kind === RuleKinds.FRAGMENT_DEFINITION && step === 3) || (kind === RuleKinds.NAMED_TYPE && prevState && (prevState.kind === RuleKinds.VARIABLE_DEFINITION || diff --git a/packages/graphql-language-service/src/parser/Rules.ts b/packages/graphql-language-service/src/parser/Rules.ts index af74ca23823..0d23a6f3010 100644 --- a/packages/graphql-language-service/src/parser/Rules.ts +++ b/packages/graphql-language-service/src/parser/Rules.ts @@ -143,7 +143,7 @@ export const ParseRules: { [name: string]: ParseRule } = { Arguments: [p('('), list('Argument'), p(')')], Argument: [name('attribute'), p(':'), 'Value'], - FragmentSpread: [p('...'), name('def'), list('Directive')], + FragmentSpread: [p('...'), name('def'), opt('Arguments'), list('Directive')], InlineFragment: [ p('...'), opt('TypeCondition'), @@ -154,6 +154,7 @@ export const ParseRules: { [name: string]: ParseRule } = { FragmentDefinition: [ word('fragment'), opt(butNot(name('def'), [word('on')])), + opt('VariableDefinitions'), 'TypeCondition', list('Directive'), 'SelectionSet', 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..c2e9be7ea3a 100644 --- a/packages/graphql-language-service/src/parser/__tests__/OnlineParser-test.ts +++ b/packages/graphql-language-service/src/parser/__tests__/OnlineParser-test.ts @@ -409,6 +409,63 @@ describe('onlineParser', () => { t.eol(); }); + + it('parses query with fragment spread containing arguments', () => { + const { t, stream } = getUtils(` + query SomeQuery { + someField { + ...fragmentName(someVariable: 6) + } + } + `); + + t.keyword('query', { kind: 'Query' }); + t.def('SomeQuery'); + t.punctuation('{', { kind: 'SelectionSet' }); + + t.property('someField', { kind: 'Field' }); + t.punctuation('{', { kind: 'SelectionSet' }); + + t.punctuation('...', { kind: 'FragmentSpread' }); + t.def('fragmentName'); + expectArgs( + { t, stream }, + { onKind: 'FragmentSpread', args: [{ name: 'someVariable', value: '6', valueType: 'Number', kind: 'NumberValue' }] }, + ); + + t.punctuation('}', { kind: 'SelectionSet' }); + + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); + + it('parses query with fragment variables', () => { + const { t, stream } = getUtils(` + fragment fragmentName($someVariable: Int) on SomeType { + anotherField + } + `); + + t.keyword('fragment', { kind: 'FragmentDefinition' }); + t.def('fragmentName'); + expectVarsDef( + { t, stream }, + { + onKind: 'FragmentDefinition', + vars: [{ name: 'someVariable', type: 'Int' }], + }, + ); + t.keyword('on', { kind: 'TypeCondition' }); + t.name('SomeType', { kind: 'NamedType' }); + t.punctuation('{', { kind: 'SelectionSet' }); + + t.property('anotherField', { kind: 'Field' }); + + t.punctuation('}', { kind: 'Document' }); + + t.eol(); + }); it('parses mutation', () => { const { t } = getUtils(` diff --git a/packages/graphql-language-service/src/parser/getTypeInfo.ts b/packages/graphql-language-service/src/parser/getTypeInfo.ts index f0f8d1ae8d7..d0f4d80f342 100644 --- a/packages/graphql-language-service/src/parser/getTypeInfo.ts +++ b/packages/graphql-language-service/src/parser/getTypeInfo.ts @@ -178,6 +178,9 @@ export function getTypeInfo( argDefs = directiveDef && (directiveDef.args as GraphQLArgument[]); break; + case RuleKinds.FRAGMENT_SPREAD: + // TODO: lookup fragment and return variable definitions (?) + break; // TODO: needs more tests case RuleKinds.ALIASED_FIELD: { const name = state.prevState?.name; diff --git a/packages/graphql-language-service/src/utils/validateWithCustomRules.ts b/packages/graphql-language-service/src/utils/validateWithCustomRules.ts index 00e39a2396a..393a188b1f0 100644 --- a/packages/graphql-language-service/src/utils/validateWithCustomRules.ts +++ b/packages/graphql-language-service/src/utils/validateWithCustomRules.ts @@ -10,6 +10,7 @@ import { ValidationRule, DocumentNode, + // TODO: this should contain fragment-arguments rules specifiedRules, validate, GraphQLError,