UNPKG

graphql-language-service-interface

Version:
450 lines (397 loc) 11.9 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 { DocumentNode, FragmentSpreadNode, FragmentDefinitionNode, TypeDefinitionNode, NamedTypeNode, ValidationRule, } from 'graphql'; import { CompletionItem, Diagnostic, Uri, IPosition, Outline, OutlineTree, GraphQLCache, } from 'graphql-language-service-types'; import { GraphQLConfig, GraphQLProjectConfig } from 'graphql-config'; import { Hover, SymbolInformation, SymbolKind, } from 'vscode-languageserver-types'; import { Kind, parse, print } from 'graphql'; import { getAutocompleteSuggestions } from './getAutocompleteSuggestions'; import { getHoverInformation, HoverConfig } from './getHoverInformation'; import { validateQuery, getRange, DIAGNOSTIC_SEVERITY } from './getDiagnostics'; import { getDefinitionQueryResultForFragmentSpread, getDefinitionQueryResultForDefinitionNode, getDefinitionQueryResultForNamedType, DefinitionQueryResult, } from './getDefinition'; import { getOutline } from './getOutline'; import { getASTNodeAtPosition } from 'graphql-language-service-utils'; const { FRAGMENT_DEFINITION, OBJECT_TYPE_DEFINITION, INTERFACE_TYPE_DEFINITION, ENUM_TYPE_DEFINITION, UNION_TYPE_DEFINITION, SCALAR_TYPE_DEFINITION, INPUT_OBJECT_TYPE_DEFINITION, SCALAR_TYPE_EXTENSION, OBJECT_TYPE_EXTENSION, INTERFACE_TYPE_EXTENSION, UNION_TYPE_EXTENSION, ENUM_TYPE_EXTENSION, INPUT_OBJECT_TYPE_EXTENSION, DIRECTIVE_DEFINITION, FRAGMENT_SPREAD, OPERATION_DEFINITION, NAMED_TYPE, } = Kind; const KIND_TO_SYMBOL_KIND: { [key: string]: SymbolKind } = { [Kind.FIELD]: SymbolKind.Field, [Kind.OPERATION_DEFINITION]: SymbolKind.Class, [Kind.FRAGMENT_DEFINITION]: SymbolKind.Class, [Kind.FRAGMENT_SPREAD]: SymbolKind.Struct, [Kind.OBJECT_TYPE_DEFINITION]: SymbolKind.Class, [Kind.ENUM_TYPE_DEFINITION]: SymbolKind.Enum, [Kind.ENUM_VALUE_DEFINITION]: SymbolKind.EnumMember, [Kind.INPUT_OBJECT_TYPE_DEFINITION]: SymbolKind.Class, [Kind.INPUT_VALUE_DEFINITION]: SymbolKind.Field, [Kind.FIELD_DEFINITION]: SymbolKind.Field, [Kind.INTERFACE_TYPE_DEFINITION]: SymbolKind.Interface, [Kind.DOCUMENT]: SymbolKind.File, // novel, for symbols only FieldWithArguments: SymbolKind.Method, }; function getKind(tree: OutlineTree) { if ( tree.kind === 'FieldDefinition' && tree.children && tree.children.length > 0 ) { return KIND_TO_SYMBOL_KIND.FieldWithArguments; } return KIND_TO_SYMBOL_KIND[tree.kind]; } export class GraphQLLanguageService { _graphQLCache: GraphQLCache; _graphQLConfig: GraphQLConfig; constructor(cache: GraphQLCache) { this._graphQLCache = cache; this._graphQLConfig = cache.getGraphQLConfig(); } getConfigForURI(uri: Uri) { const config = this._graphQLCache.getProjectForFile(uri); if (config) { return config; } throw Error(`No config found for uri: ${uri}`); } public async getDiagnostics( query: string, uri: Uri, isRelayCompatMode?: boolean, ): Promise<Array<Diagnostic>> { // Perform syntax diagnostics first, as this doesn't require // schema/fragment definitions, even the project configuration. let queryHasExtensions = false; const projectConfig = this.getConfigForURI(uri); if (!projectConfig) { return []; } const { schema: schemaPath, name: projectName, extensions } = projectConfig; try { const queryAST = parse(query); if (!schemaPath || uri !== schemaPath) { queryHasExtensions = queryAST.definitions.some(definition => { switch (definition.kind) { case OBJECT_TYPE_DEFINITION: case INTERFACE_TYPE_DEFINITION: case ENUM_TYPE_DEFINITION: case UNION_TYPE_DEFINITION: case SCALAR_TYPE_DEFINITION: case INPUT_OBJECT_TYPE_DEFINITION: case SCALAR_TYPE_EXTENSION: case OBJECT_TYPE_EXTENSION: case INTERFACE_TYPE_EXTENSION: case UNION_TYPE_EXTENSION: case ENUM_TYPE_EXTENSION: case INPUT_OBJECT_TYPE_EXTENSION: case DIRECTIVE_DEFINITION: return true; } return false; }); } } catch (error) { const range = getRange(error.locations[0], query); return [ { severity: DIAGNOSTIC_SEVERITY.Error, message: error.message, source: 'GraphQL: Syntax', range, }, ]; } // If there's a matching config, proceed to prepare to run validation let source = query; const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( projectConfig, ); const fragmentDependencies = await this._graphQLCache.getFragmentDependencies( query, fragmentDefinitions, ); const dependenciesSource = fragmentDependencies.reduce( (prev, cur) => `${prev} ${print(cur.definition)}`, '', ); source = `${source} ${dependenciesSource}`; let validationAst = null; try { validationAst = parse(source); } catch (error) { // the query string is already checked to be parsed properly - errors // from this parse must be from corrupted fragment dependencies. // For IDEs we don't care for errors outside of the currently edited // query, so we return an empty array here. return []; } // Check if there are custom validation rules to be used let customRules: ValidationRule[] | null = null; if ( extensions?.customValidationRules && typeof extensions.customValidationRules === 'function' ) { customRules = extensions.customValidationRules(this._graphQLConfig); /* eslint-enable no-implicit-coercion */ } const schema = await this._graphQLCache.getSchema( projectName, queryHasExtensions, ); if (!schema) { return []; } return validateQuery( validationAst, schema, customRules as ValidationRule[], isRelayCompatMode, ); } public async getAutocompleteSuggestions( query: string, position: IPosition, filePath: Uri, ): Promise<Array<CompletionItem>> { const projectConfig = this.getConfigForURI(filePath); const schema = await this._graphQLCache.getSchema(projectConfig.name); const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( projectConfig, ); const fragmentInfo = Array.from(fragmentDefinitions).map( ([, info]) => info.definition, ); if (schema) { return getAutocompleteSuggestions( schema, query, position, undefined, fragmentInfo, ); } return []; } public async getHoverInformation( query: string, position: IPosition, filePath: Uri, options?: HoverConfig, ): Promise<Hover['contents']> { const projectConfig = this.getConfigForURI(filePath); const schema = await this._graphQLCache.getSchema(projectConfig.name); if (schema) { return getHoverInformation(schema, query, position, undefined, options); } return ''; } public async getDefinition( query: string, position: IPosition, filePath: Uri, ): Promise<DefinitionQueryResult | null> { const projectConfig = this.getConfigForURI(filePath); let ast; try { ast = parse(query); } catch (error) { return null; } const node = getASTNodeAtPosition(query, ast, position); if (node) { switch (node.kind) { case FRAGMENT_SPREAD: return this._getDefinitionForFragmentSpread( query, ast, node, filePath, projectConfig, ); case FRAGMENT_DEFINITION: case OPERATION_DEFINITION: return getDefinitionQueryResultForDefinitionNode( filePath, query, node, ); case NAMED_TYPE: return this._getDefinitionForNamedType( query, ast, node, filePath, projectConfig, ); } } return null; } public async getDocumentSymbols( document: string, filePath: Uri, ): Promise<SymbolInformation[]> { const outline = await this.getOutline(document); if (!outline) { return []; } const output: Array<SymbolInformation> = []; const input = outline.outlineTrees.map((tree: OutlineTree) => [null, tree]); while (input.length > 0) { const res = input.pop(); if (!res) { return []; } const [parent, tree] = res; if (!tree) { return []; } output.push({ // @ts-ignore name: tree.representativeName, kind: getKind(tree), location: { uri: filePath, range: { start: tree.startPosition, // @ts-ignore end: tree.endPosition, }, }, containerName: parent ? parent.representativeName : undefined, }); input.push(...tree.children.map(child => [tree, child])); } return output; } // // public async getReferences( // document: string, // position: Position, // filePath: Uri, // ): Promise<Location[]> { // // } async _getDefinitionForNamedType( query: string, ast: DocumentNode, node: NamedTypeNode, filePath: Uri, projectConfig: GraphQLProjectConfig, ): Promise<DefinitionQueryResult | null> { const objectTypeDefinitions = await this._graphQLCache.getObjectTypeDefinitions( projectConfig, ); const dependencies = await this._graphQLCache.getObjectTypeDependenciesForAST( ast, objectTypeDefinitions, ); const localObjectTypeDefinitions = ast.definitions.filter( definition => definition.kind === OBJECT_TYPE_DEFINITION || definition.kind === INPUT_OBJECT_TYPE_DEFINITION || definition.kind === ENUM_TYPE_DEFINITION || definition.kind === SCALAR_TYPE_DEFINITION || definition.kind === INTERFACE_TYPE_DEFINITION, ); const typeCastedDefs = (localObjectTypeDefinitions as any) as Array< TypeDefinitionNode >; const localOperationDefinationInfos = typeCastedDefs.map( (definition: TypeDefinitionNode) => ({ filePath, content: query, definition, }), ); const result = await getDefinitionQueryResultForNamedType( query, node, dependencies.concat(localOperationDefinationInfos), ); return result; } async _getDefinitionForFragmentSpread( query: string, ast: DocumentNode, node: FragmentSpreadNode, filePath: Uri, projectConfig: GraphQLProjectConfig, ): Promise<DefinitionQueryResult | null> { const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( projectConfig, ); const dependencies = await this._graphQLCache.getFragmentDependenciesForAST( ast, fragmentDefinitions, ); const localFragDefinitions = ast.definitions.filter( definition => definition.kind === FRAGMENT_DEFINITION, ); const typeCastedDefs = (localFragDefinitions as any) as Array< FragmentDefinitionNode >; const localFragInfos = typeCastedDefs.map( (definition: FragmentDefinitionNode) => ({ filePath, content: query, definition, }), ); const result = await getDefinitionQueryResultForFragmentSpread( query, node, dependencies.concat(localFragInfos), ); return result; } async getOutline(documentText: string): Promise<Outline | null> { return getOutline(documentText); } }