UNPKG

graphql-language-service-interface

Version:
1,081 lines (1,005 loc) 31.3 kB
/** * Copyright (c) 2021 GraphQL Contributors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import { CompletionItemKind, InsertTextFormat, } from 'vscode-languageserver-types'; import { FragmentDefinitionNode, GraphQLDirective, GraphQLSchema, GraphQLType, GraphQLCompositeType, GraphQLEnumValue, GraphQLField, GraphQLFieldMap, GraphQLNamedType, isInterfaceType, GraphQLInterfaceType, GraphQLObjectType, Kind, DirectiveLocation, GraphQLArgument, isListType, isNonNullType, } from 'graphql'; import { CompletionItem, AllTypeInfo, IPosition, } from 'graphql-language-service-types'; import { GraphQLBoolean, GraphQLEnumType, GraphQLInputObjectType, GraphQLList, SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, assertAbstractType, doTypesOverlap, getNamedType, getNullableType, isAbstractType, isCompositeType, isInputType, visit, parse, } from 'graphql'; import { CharacterStream, onlineParser, ContextToken, State, RuleKinds, RuleKind, _RuleKinds, ContextTokenForCodeMirror, } from 'graphql-language-service-parser'; import { forEachState, getDefinitionState, getFieldDef, hintList, objectValues, } from './autocompleteUtils'; export const SuggestionCommand = { command: 'editor.action.triggerSuggest', title: 'Suggestions', }; const collectFragmentDefs = (op: string | undefined) => { const externalFragments: FragmentDefinitionNode[] = []; if (op) { visit(parse(op), { FragmentDefinition(def) { externalFragments.push(def); }, }); } return externalFragments; }; export type AutocompleteSuggestionOptions = { fillLeafsOnComplete?: boolean; schema?: GraphQLSchema; }; /** * Given GraphQLSchema, queryText, and context of the current position within * the source text, provide a list of typeahead entries. */ export function getAutocompleteSuggestions( schema: GraphQLSchema, queryText: string, cursor: IPosition, contextToken?: ContextTokenForCodeMirror, fragmentDefs?: FragmentDefinitionNode[] | string, options?: AutocompleteSuggestionOptions, ): Array<CompletionItem> { const opts = { ...options, schema, }; const token: ContextToken = contextToken || getTokenAtPosition(queryText, cursor); const state = token.state.kind === 'Invalid' ? token.state.prevState : token.state; // relieve flow errors by checking if `state` exists if (!state) { return []; } const kind = state.kind; const step = state.step; const typeInfo = getTypeInfo(schema, token.state); // Definition kinds if (kind === RuleKinds.DOCUMENT) { return hintList(token, [ { label: 'query', kind: CompletionItemKind.Function }, { label: 'mutation', kind: CompletionItemKind.Function }, { label: 'subscription', kind: CompletionItemKind.Function }, { label: 'fragment', kind: CompletionItemKind.Function }, { label: '{', kind: CompletionItemKind.Constructor }, ]); } if ( kind === RuleKinds.IMPLEMENTS || (kind === RuleKinds.NAMED_TYPE && state.prevState?.kind === RuleKinds.IMPLEMENTS) ) { return getSuggestionsForImplements( token, state, schema, queryText, typeInfo, ); } // Field names if ( kind === RuleKinds.SELECTION_SET || kind === RuleKinds.FIELD || kind === RuleKinds.ALIASED_FIELD ) { return getSuggestionsForFieldNames(token, typeInfo, opts); } // Argument names if ( kind === RuleKinds.ARGUMENTS || (kind === RuleKinds.ARGUMENT && step === 0) ) { const argDefs = typeInfo.argDefs; if (argDefs) { return hintList( token, argDefs.map( (argDef: GraphQLArgument): CompletionItem => ({ label: argDef.name, insertText: argDef.name + ': ', command: SuggestionCommand, detail: String(argDef.type), documentation: argDef.description ?? undefined, kind: CompletionItemKind.Variable, type: argDef.type, }), ), ); } } // Input Object fields if ( kind === RuleKinds.OBJECT_VALUE || (kind === RuleKinds.OBJECT_FIELD && step === 0) ) { if (typeInfo.objectFieldDefs) { const objectFields = objectValues(typeInfo.objectFieldDefs); const completionKind = kind === RuleKinds.OBJECT_VALUE ? CompletionItemKind.Value : CompletionItemKind.Field; return hintList( token, objectFields.map(field => ({ label: field.name, detail: String(field.type), documentation: field.description ?? undefined, kind: completionKind, type: field.type, })), ); } } // Input values: Enum and Boolean if ( kind === RuleKinds.ENUM_VALUE || (kind === RuleKinds.LIST_VALUE && step === 1) || (kind === RuleKinds.OBJECT_FIELD && step === 2) || (kind === RuleKinds.ARGUMENT && step === 2) ) { return getSuggestionsForInputValues(token, typeInfo, queryText, schema); } // complete for all variables available in the query if (kind === RuleKinds.VARIABLE && step === 1) { const namedInputType = getNamedType(typeInfo.inputType as GraphQLType); const variableDefinitions = getVariableCompletions( queryText, schema, token, ); return hintList( token, variableDefinitions.filter(v => v.detail === namedInputType?.name), ); } // Fragment type conditions if ( (kind === RuleKinds.TYPE_CONDITION && step === 1) || (kind === RuleKinds.NAMED_TYPE && state.prevState != null && state.prevState.kind === RuleKinds.TYPE_CONDITION) ) { return getSuggestionsForFragmentTypeConditions( token, typeInfo, schema, kind, ); } // Fragment spread names if (kind === RuleKinds.FRAGMENT_SPREAD && step === 1) { return getSuggestionsForFragmentSpread( token, typeInfo, schema, queryText, Array.isArray(fragmentDefs) ? fragmentDefs : collectFragmentDefs(fragmentDefs), ); } // Variable definition types if ( (kind === RuleKinds.VARIABLE_DEFINITION && step === 2) || (kind === RuleKinds.LIST_TYPE && step === 1) || (kind === RuleKinds.NAMED_TYPE && state.prevState && (state.prevState.kind === RuleKinds.VARIABLE_DEFINITION || state.prevState.kind === RuleKinds.LIST_TYPE || state.prevState.kind === RuleKinds.NON_NULL_TYPE)) ) { return getSuggestionsForVariableDefinition(token, schema, kind); } // Directive names if (kind === RuleKinds.DIRECTIVE) { return getSuggestionsForDirective(token, state, schema, kind); } return []; } const insertSuffix = ` {\n $1\n}`; /** * Choose carefully when to insert the `insertText`! * @param field * @returns */ const getInsertText = (field: GraphQLField<null, null>) => { const type = field.type; if (isCompositeType(type)) { return insertSuffix; } if (isListType(type) && isCompositeType(type.ofType)) { return insertSuffix; } if (isNonNullType(type)) { if (isCompositeType(type.ofType)) { return insertSuffix; } if (isListType(type.ofType) && isCompositeType(type.ofType.ofType)) { return insertSuffix; } } return null; }; // Helper functions to get suggestions for each kinds function getSuggestionsForFieldNames( token: ContextToken, typeInfo: AllTypeInfo, options?: AutocompleteSuggestionOptions, ): Array<CompletionItem> { if (typeInfo.parentType) { const parentType = typeInfo.parentType; let fields: GraphQLField<null, null>[] = []; if ('getFields' in parentType) { fields = objectValues<GraphQLField<null, null>>( // TODO: getFields returns `GraphQLFieldMap<any, any> | GraphQLInputFieldMap` parentType.getFields() as GraphQLFieldMap<any, any>, ); } if (isCompositeType(parentType)) { fields.push(TypeNameMetaFieldDef); } if (parentType === options?.schema?.getQueryType()) { fields.push(SchemaMetaFieldDef, TypeMetaFieldDef); } return hintList( token, fields.map<CompletionItem>((field, index) => { const suggestion: CompletionItem = { // This will sort the fields in the same order they are listed in the schema sortText: String(index) + field.name, label: field.name, detail: String(field.type), documentation: field.description ?? undefined, deprecated: Boolean(field.deprecationReason), isDeprecated: Boolean(field.deprecationReason), deprecationReason: field.deprecationReason, kind: CompletionItemKind.Field, type: field.type, }; // TODO: fillLeafs capability const insertText = getInsertText(field); if (insertText) { suggestion.insertText = field.name + insertText; suggestion.insertTextFormat = InsertTextFormat.Snippet; suggestion.command = SuggestionCommand; } return suggestion; }), ); } return []; } function getSuggestionsForInputValues( token: ContextToken, typeInfo: AllTypeInfo, queryText: string, schema: GraphQLSchema, ): Array<CompletionItem> { const namedInputType = getNamedType(typeInfo.inputType as GraphQLType); const queryVariables: CompletionItem[] = getVariableCompletions( queryText, schema, token, ).filter(v => v.detail === namedInputType.name); if (namedInputType instanceof GraphQLEnumType) { const values = namedInputType.getValues(); return hintList( token, values .map<CompletionItem>((value: GraphQLEnumValue) => ({ label: value.name, detail: String(namedInputType), documentation: value.description ?? undefined, deprecated: Boolean(value.deprecationReason), isDeprecated: Boolean(value.deprecationReason), deprecationReason: value.deprecationReason, kind: CompletionItemKind.EnumMember, type: namedInputType, })) .concat(queryVariables), ); } else if (namedInputType === GraphQLBoolean) { return hintList( token, queryVariables.concat([ { label: 'true', detail: String(GraphQLBoolean), documentation: 'Not false.', kind: CompletionItemKind.Variable, type: GraphQLBoolean, }, { label: 'false', detail: String(GraphQLBoolean), documentation: 'Not true.', kind: CompletionItemKind.Variable, type: GraphQLBoolean, }, ]), ); } return queryVariables; } function getSuggestionsForImplements( token: ContextToken, tokenState: State, schema: GraphQLSchema, documentText: string, typeInfo: AllTypeInfo, ): Array<CompletionItem> { // exit empty if we need an & if (tokenState.needsSeperator) { return []; } const typeMap = schema.getTypeMap(); const schemaInterfaces = objectValues(typeMap).filter(isInterfaceType); const schemaInterfaceNames = schemaInterfaces.map(({ name }) => name); const inlineInterfaces: Set<string> = new Set(); runOnlineParser(documentText, (_, state: State) => { if (state.name) { // gather inline interface definitions if ( state.kind === RuleKinds.INTERFACE_DEF && !schemaInterfaceNames.includes(state.name) ) { inlineInterfaces.add(<string>state.name); } // gather the other interfaces the current type/interface definition implements // so we can filter them out below if ( state.kind === RuleKinds.NAMED_TYPE && state.prevState?.kind === RuleKinds.IMPLEMENTS ) { if (typeInfo.interfaceDef) { const existingType = typeInfo.interfaceDef ?.getInterfaces() .find(({ name }) => name === state.name); if (existingType) { return; } const type = schema.getType(state.name); const interfaceConfig = typeInfo.interfaceDef?.toConfig()!; typeInfo.interfaceDef = new GraphQLInterfaceType({ ...interfaceConfig, interfaces: [ ...interfaceConfig.interfaces, (type as GraphQLInterfaceType) || new GraphQLInterfaceType({ name: state.name, fields: {} }), ], }); } else if (typeInfo.objectTypeDef) { const existingType = typeInfo.objectTypeDef ?.getInterfaces() .find(({ name }) => name === state.name); if (existingType) { return; } const type = schema.getType(state.name); const objectTypeConfig = typeInfo.objectTypeDef?.toConfig()!; typeInfo.objectTypeDef = new GraphQLObjectType({ ...objectTypeConfig, interfaces: [ ...objectTypeConfig.interfaces, (type as GraphQLInterfaceType) || new GraphQLInterfaceType({ name: state.name, fields: {} }), ], }); } } } }); const currentTypeToExtend = typeInfo.interfaceDef || typeInfo.objectTypeDef; const siblingInterfaces = currentTypeToExtend?.getInterfaces() || []; const siblingInterfaceNames = siblingInterfaces.map(({ name }) => name); // TODO: we should be using schema.getPossibleTypes() here, but const possibleInterfaces = schemaInterfaces .concat( [...inlineInterfaces].map(name => ({ name } as GraphQLInterfaceType)), ) .filter( ({ name }) => name !== currentTypeToExtend?.name && !siblingInterfaceNames.includes(name), ); return hintList( token, possibleInterfaces.map(type => { const result = { label: type.name, kind: CompletionItemKind.Interface, type, } as CompletionItem; if (type?.description) { result.documentation = type.description; } // TODO: should we report what an interface implements in CompletionItem.detail? // result.detail = 'Interface' // const interfaces = type.astNode?.interfaces; // if (interfaces && interfaces.length > 0) { // result.detail += ` (implements ${interfaces // .map(i => i.name.value) // .join(' & ')})`; // } return result; }), ); } function getSuggestionsForFragmentTypeConditions( token: ContextToken, typeInfo: AllTypeInfo, schema: GraphQLSchema, _kind: 'NamedType' | 'TypeCondition', ): Array<CompletionItem> { let possibleTypes: GraphQLType[]; if (typeInfo.parentType) { if (isAbstractType(typeInfo.parentType)) { const abstractType = assertAbstractType(typeInfo.parentType); // Collect both the possible Object types as well as the interfaces // they implement. const possibleObjTypes = schema.getPossibleTypes(abstractType); const possibleIfaceMap = Object.create(null); possibleObjTypes.forEach(type => { type.getInterfaces().forEach(iface => { possibleIfaceMap[iface.name] = iface; }); }); possibleTypes = possibleObjTypes.concat(objectValues(possibleIfaceMap)); } else { // The parent type is a non-abstract Object type, so the only possible // type that can be used is that same type. possibleTypes = [typeInfo.parentType]; } } else { const typeMap = schema.getTypeMap(); possibleTypes = objectValues(typeMap).filter(isCompositeType); } return hintList( token, possibleTypes.map(type => { const namedType = getNamedType(type); return { label: String(type), documentation: (namedType && namedType.description) || '', kind: CompletionItemKind.Field, }; }), ); } function getSuggestionsForFragmentSpread( token: ContextToken, typeInfo: AllTypeInfo, schema: GraphQLSchema, queryText: string, fragmentDefs?: FragmentDefinitionNode[], ): Array<CompletionItem> { if (!queryText) { return []; } const typeMap = schema.getTypeMap(); const defState = getDefinitionState(token.state); const fragments = getFragmentDefinitions(queryText); if (fragmentDefs && fragmentDefs.length > 0) { fragments.push(...fragmentDefs); } // Filter down to only the fragments which may exist here. const relevantFrags = fragments.filter( frag => // Only include fragments with known types. typeMap[frag.typeCondition.name.value] && // Only include fragments which are not cyclic. !( defState && defState.kind === RuleKinds.FRAGMENT_DEFINITION && defState.name === frag.name.value ) && // Only include fragments which could possibly be spread here. isCompositeType(typeInfo.parentType) && isCompositeType(typeMap[frag.typeCondition.name.value]) && doTypesOverlap( schema, typeInfo.parentType, typeMap[frag.typeCondition.name.value] as GraphQLCompositeType, ), ); return hintList( token, relevantFrags.map(frag => ({ label: frag.name.value, detail: String(typeMap[frag.typeCondition.name.value]), documentation: `fragment ${frag.name.value} on ${frag.typeCondition.name.value}`, kind: CompletionItemKind.Field, type: typeMap[frag.typeCondition.name.value], })), ); } // TODO: should be using getTypeInfo() for this if we can const getParentDefinition = (state: State, kind: RuleKind) => { if (state.prevState?.kind === kind) { return state.prevState; } if (state.prevState?.prevState?.kind === kind) { return state.prevState.prevState; } if (state.prevState?.prevState?.prevState?.kind === kind) { return state.prevState.prevState.prevState; } if (state.prevState?.prevState?.prevState?.prevState?.kind === kind) { return state.prevState.prevState.prevState.prevState; } }; export function getVariableCompletions( queryText: string, schema: GraphQLSchema, token: ContextToken, ): CompletionItem[] { let variableName: null | string = null; let variableType: GraphQLInputObjectType | undefined | null; const definitions: Record<string, any> = Object.create({}); runOnlineParser(queryText, (_, state: State) => { // TODO: gather this as part of `AllTypeInfo`, as I don't think it's optimal to re-run the parser like this if (state?.kind === RuleKinds.VARIABLE && state.name) { variableName = state.name; } if (state?.kind === RuleKinds.NAMED_TYPE && variableName) { const parentDefinition = getParentDefinition(state, RuleKinds.TYPE); if (parentDefinition?.type) { variableType = schema.getType( parentDefinition?.type, ) as GraphQLInputObjectType; } } if (variableName && variableType) { if (!definitions[variableName]) { // append `$` if the `token.string` is not already `$` definitions[variableName] = { detail: variableType.toString(), insertText: token.string === '$' ? variableName : '$' + variableName, label: variableName, // keep label the same for `codemirror-graphql` type: variableType, kind: CompletionItemKind.Variable, } as CompletionItem; variableName = null; variableType = null; } } }); return objectValues(definitions); } export function getFragmentDefinitions( queryText: string, ): Array<FragmentDefinitionNode> { const fragmentDefs: FragmentDefinitionNode[] = []; runOnlineParser(queryText, (_, state: State) => { if ( state.kind === RuleKinds.FRAGMENT_DEFINITION && state.name && state.type ) { fragmentDefs.push({ kind: RuleKinds.FRAGMENT_DEFINITION, name: { kind: Kind.NAME, value: state.name, }, selectionSet: { kind: RuleKinds.SELECTION_SET, selections: [], }, typeCondition: { kind: RuleKinds.NAMED_TYPE, name: { kind: Kind.NAME, value: state.type, }, }, }); } }); return fragmentDefs; } function getSuggestionsForVariableDefinition( token: ContextToken, schema: GraphQLSchema, _kind: string, ): Array<CompletionItem> { const inputTypeMap = schema.getTypeMap(); const inputTypes = objectValues(inputTypeMap).filter(isInputType); return hintList( token, // TODO: couldn't get Exclude<> working here inputTypes.map((type: GraphQLNamedType) => ({ label: type.name, documentation: type.description as string, kind: CompletionItemKind.Variable, })), ); } function getSuggestionsForDirective( token: ContextToken, state: State, schema: GraphQLSchema, _kind: string, ): Array<CompletionItem> { if (state.prevState && state.prevState.kind) { const directives = schema .getDirectives() .filter(directive => canUseDirective(state.prevState, directive)); return hintList( token, directives.map(directive => ({ label: directive.name, documentation: directive.description || '', kind: CompletionItemKind.Function, })), ); } return []; } export function getTokenAtPosition( queryText: string, cursor: IPosition, ): ContextToken { let styleAtCursor = null; let stateAtCursor = null; let stringAtCursor = null; const token = runOnlineParser(queryText, (stream, state, style, index) => { if (index === cursor.line) { if (stream.getCurrentPosition() >= cursor.character) { styleAtCursor = style; stateAtCursor = { ...state }; stringAtCursor = stream.current(); return 'BREAK'; } } }); // Return the state/style of parsed token in case those at cursor aren't // available. return { start: token.start, end: token.end, string: stringAtCursor || token.string, state: stateAtCursor || token.state, style: styleAtCursor || token.style, }; } /** * Provides an utility function to parse a given query text and construct a * `token` context object. * A token context provides useful information about the token/style that * CharacterStream currently possesses, as well as the end state and style * of the token. */ type callbackFnType = ( stream: CharacterStream, state: State, style: string, index: number, ) => void | 'BREAK'; export function runOnlineParser( queryText: string, callback: callbackFnType, ): ContextToken { const lines = queryText.split('\n'); const parser = onlineParser(); let state = parser.startState(); let style = ''; let stream: CharacterStream = new CharacterStream(''); for (let i = 0; i < lines.length; i++) { stream = new CharacterStream(lines[i]); while (!stream.eol()) { style = parser.token(stream, state); const code = callback(stream, state, style, i); if (code === 'BREAK') { break; } } // Above while loop won't run if there is an empty line. // Run the callback one more time to catch this. callback(stream, state, style, i); if (!state.kind) { state = parser.startState(); } } return { start: stream.getStartOfToken(), end: stream.getCurrentPosition(), string: stream.current(), state, style, }; } export function canUseDirective( state: State['prevState'], directive: GraphQLDirective, ): boolean { if (!state || !state.kind) { return false; } const kind = state.kind; const locations = directive.locations; switch (kind) { case RuleKinds.QUERY: return locations.indexOf(DirectiveLocation.QUERY) !== -1; case RuleKinds.MUTATION: return locations.indexOf(DirectiveLocation.MUTATION) !== -1; case RuleKinds.SUBSCRIPTION: return locations.indexOf(DirectiveLocation.SUBSCRIPTION) !== -1; case RuleKinds.FIELD: case RuleKinds.ALIASED_FIELD: return locations.indexOf(DirectiveLocation.FIELD) !== -1; case RuleKinds.FRAGMENT_DEFINITION: return locations.indexOf(DirectiveLocation.FRAGMENT_DEFINITION) !== -1; case RuleKinds.FRAGMENT_SPREAD: return locations.indexOf(DirectiveLocation.FRAGMENT_SPREAD) !== -1; case RuleKinds.INLINE_FRAGMENT: return locations.indexOf(DirectiveLocation.INLINE_FRAGMENT) !== -1; // Schema Definitions case RuleKinds.SCHEMA_DEF: return locations.indexOf(DirectiveLocation.SCHEMA) !== -1; case RuleKinds.SCALAR_DEF: return locations.indexOf(DirectiveLocation.SCALAR) !== -1; case RuleKinds.OBJECT_TYPE_DEF: return locations.indexOf(DirectiveLocation.OBJECT) !== -1; case RuleKinds.FIELD_DEF: return locations.indexOf(DirectiveLocation.FIELD_DEFINITION) !== -1; case RuleKinds.INTERFACE_DEF: return locations.indexOf(DirectiveLocation.INTERFACE) !== -1; case RuleKinds.UNION_DEF: return locations.indexOf(DirectiveLocation.UNION) !== -1; case RuleKinds.ENUM_DEF: return locations.indexOf(DirectiveLocation.ENUM) !== -1; case RuleKinds.ENUM_VALUE: return locations.indexOf(DirectiveLocation.ENUM_VALUE) !== -1; case RuleKinds.INPUT_DEF: return locations.indexOf(DirectiveLocation.INPUT_OBJECT) !== -1; case RuleKinds.INPUT_VALUE_DEF: const prevStateKind = state.prevState && state.prevState.kind; switch (prevStateKind) { case RuleKinds.ARGUMENTS_DEF: return ( locations.indexOf(DirectiveLocation.ARGUMENT_DEFINITION) !== -1 ); case RuleKinds.INPUT_DEF: return ( locations.indexOf(DirectiveLocation.INPUT_FIELD_DEFINITION) !== -1 ); } } return false; } // Utility for collecting rich type information given any token's state // from the graphql-mode parser. export function getTypeInfo( schema: GraphQLSchema, tokenState: State, ): AllTypeInfo { let argDef: AllTypeInfo['argDef']; let argDefs: AllTypeInfo['argDefs']; let directiveDef: AllTypeInfo['directiveDef']; let enumValue: AllTypeInfo['enumValue']; let fieldDef: AllTypeInfo['fieldDef']; let inputType: AllTypeInfo['inputType']; let objectTypeDef: AllTypeInfo['objectTypeDef']; let objectFieldDefs: AllTypeInfo['objectFieldDefs']; let parentType: AllTypeInfo['parentType']; let type: AllTypeInfo['type']; let interfaceDef: AllTypeInfo['interfaceDef']; forEachState(tokenState, state => { switch (state.kind) { case RuleKinds.QUERY: case 'ShortQuery': type = schema.getQueryType(); break; case RuleKinds.MUTATION: type = schema.getMutationType(); break; case RuleKinds.SUBSCRIPTION: type = schema.getSubscriptionType(); break; case RuleKinds.INLINE_FRAGMENT: case RuleKinds.FRAGMENT_DEFINITION: if (state.type) { type = schema.getType(state.type); } break; case RuleKinds.FIELD: case RuleKinds.ALIASED_FIELD: { if (!type || !state.name) { fieldDef = null; } else { fieldDef = parentType ? getFieldDef(schema, parentType, state.name) : null; type = fieldDef ? fieldDef.type : null; } break; } case RuleKinds.SELECTION_SET: parentType = getNamedType(type as GraphQLType); break; case RuleKinds.DIRECTIVE: directiveDef = state.name ? schema.getDirective(state.name) : null; break; case RuleKinds.INTERFACE_DEF: if (state.name) { objectTypeDef = null; interfaceDef = new GraphQLInterfaceType({ name: state.name, interfaces: [], fields: {}, }); } break; case RuleKinds.OBJECT_TYPE_DEF: if (state.name) { interfaceDef = null; objectTypeDef = new GraphQLObjectType({ name: state.name, interfaces: [], fields: {}, }); } break; case RuleKinds.ARGUMENTS: { if (!state.prevState) { argDefs = null; } else { switch (state.prevState.kind) { case RuleKinds.FIELD: argDefs = fieldDef && (fieldDef.args as GraphQLArgument[]); break; case RuleKinds.DIRECTIVE: argDefs = directiveDef && (directiveDef.args as GraphQLArgument[]); break; // TODO: needs more tests case RuleKinds.ALIASED_FIELD: { const name = state.prevState && state.prevState.name; if (!name) { argDefs = null; break; } const field = parentType ? getFieldDef(schema, parentType, name) : null; if (!field) { argDefs = null; break; } argDefs = field.args as GraphQLArgument[]; break; } default: argDefs = null; break; } } break; } case RuleKinds.ARGUMENT: if (argDefs) { for (let i = 0; i < argDefs.length; i++) { if (argDefs[i].name === state.name) { argDef = argDefs[i]; break; } } } inputType = argDef && argDef.type; break; // TODO: needs tests case RuleKinds.ENUM_VALUE: const enumType = getNamedType(inputType as GraphQLType); enumValue = enumType instanceof GraphQLEnumType ? enumType .getValues() .find((val: GraphQLEnumValue) => val.value === state.name) : null; break; // TODO: needs tests case RuleKinds.LIST_VALUE: const nullableType = getNullableType(inputType as GraphQLType); inputType = nullableType instanceof GraphQLList ? nullableType.ofType : null; break; case RuleKinds.OBJECT_VALUE: const objectType = getNamedType(inputType as GraphQLType); objectFieldDefs = objectType instanceof GraphQLInputObjectType ? objectType.getFields() : null; break; // TODO: needs tests case RuleKinds.OBJECT_FIELD: const objectField = state.name && objectFieldDefs ? objectFieldDefs[state.name] : null; inputType = objectField && objectField.type; break; case RuleKinds.NAMED_TYPE: if (state.name) { type = schema.getType(state.name); } // TODO: collect already extended interfaces of the type/interface we're extending // here to eliminate them from the completion list // because "type A extends B & C &" should not show completion options for B & C still. break; } }); return { argDef, argDefs, directiveDef, enumValue, fieldDef, inputType, objectFieldDefs, parentType, type, interfaceDef, objectTypeDef, }; }