graphql-language-service-server
Version:
Server process backing the GraphQL Language Service
546 lines (486 loc) • 14.6 kB
text/typescript
/**
* 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,
FieldNode,
GraphQLError,
Kind,
parse,
print,
isTypeDefinitionNode,
ArgumentNode,
typeFromAST,
} from 'graphql';
import {
CompletionItem,
Diagnostic,
Uri,
IPosition,
Outline,
OutlineTree,
getAutocompleteSuggestions,
getHoverInformation,
HoverConfig,
validateQuery,
getRange,
DIAGNOSTIC_SEVERITY,
getOutline,
getDefinitionQueryResultForFragmentSpread,
getDefinitionQueryResultForDefinitionNode,
getDefinitionQueryResultForNamedType,
getDefinitionQueryResultForField,
getASTNodeAtPosition,
getTokenAtPosition,
getTypeInfo,
DefinitionQueryResponse,
getDefinitionQueryResultForArgument,
} from 'graphql-language-service';
import type { GraphQLCache } from './GraphQLCache';
import { GraphQLConfig, GraphQLProjectConfig } from 'graphql-config';
import type { Logger } from 'vscode-languageserver';
import {
Hover,
SymbolInformation,
SymbolKind,
} from 'vscode-languageserver-types';
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;
_logger: Logger;
constructor(cache: GraphQLCache, logger: Logger) {
this._graphQLCache = cache;
this._graphQLConfig = cache.getGraphQLConfig();
this._logger = logger;
}
getConfigForURI(uri: Uri) {
const config = this._graphQLCache.getProjectForFile(uri);
if (config) {
return config;
}
}
public async getDiagnostics(
document: 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 documentHasExtensions = false;
const projectConfig = this.getConfigForURI(uri);
// skip validation when there's nothing to validate, prevents noisy unexpected EOF errors
if (!projectConfig || !document || document.trim().length < 2) {
return [];
}
const { schema: schemaPath, name: projectName, extensions } = projectConfig;
try {
const documentAST = parse(document);
if (!schemaPath || uri !== schemaPath) {
documentHasExtensions = documentAST.definitions.some(definition => {
switch (definition.kind) {
case Kind.OBJECT_TYPE_DEFINITION:
case Kind.INTERFACE_TYPE_DEFINITION:
case Kind.ENUM_TYPE_DEFINITION:
case Kind.UNION_TYPE_DEFINITION:
case Kind.SCALAR_TYPE_DEFINITION:
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
case Kind.SCALAR_TYPE_EXTENSION:
case Kind.OBJECT_TYPE_EXTENSION:
case Kind.INTERFACE_TYPE_EXTENSION:
case Kind.UNION_TYPE_EXTENSION:
case Kind.ENUM_TYPE_EXTENSION:
case Kind.INPUT_OBJECT_TYPE_EXTENSION:
case Kind.DIRECTIVE_DEFINITION:
return true;
}
return false;
});
}
} catch (error) {
if (error instanceof GraphQLError) {
const range = getRange(
error.locations?.[0] ?? { column: 0, line: 0 },
document,
);
return [
{
severity: DIAGNOSTIC_SEVERITY.Error,
message: error.message,
source: 'GraphQL: Syntax',
range,
},
];
}
throw error;
}
// If there's a matching config, proceed to prepare to run validation
let source = document;
const fragmentDefinitions =
await this._graphQLCache.getFragmentDefinitions(projectConfig);
const fragmentDependencies =
await this._graphQLCache.getFragmentDependencies(
document,
fragmentDefinitions,
);
const dependenciesSource = fragmentDependencies.reduce(
(prev, cur) => `${prev} ${print(cur.definition)}`,
'',
);
source = `${source} ${dependenciesSource}`;
let validationAst = null;
try {
validationAst = parse(source);
} catch {
// 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);
}
const schema = await this._graphQLCache.getSchema(
projectName,
documentHasExtensions,
);
if (!schema) {
return [];
}
return validateQuery(validationAst, schema, customRules, isRelayCompatMode);
}
public async getAutocompleteSuggestions(
query: string,
position: IPosition,
filePath: Uri,
): Promise<Array<CompletionItem>> {
const projectConfig = this.getConfigForURI(filePath);
if (!projectConfig) {
return [];
}
const schema = await this._graphQLCache.getSchema(projectConfig.name);
if (!schema) {
return [];
}
let fragmentInfo = [] as Array<FragmentDefinitionNode>;
try {
const fragmentDefinitions =
await this._graphQLCache.getFragmentDefinitions(projectConfig);
fragmentInfo = Array.from(fragmentDefinitions).map(
([, info]) => info.definition,
);
} catch {}
return getAutocompleteSuggestions(
schema,
query,
position,
undefined,
fragmentInfo,
{
uri: filePath,
fillLeafsOnComplete:
projectConfig?.extensions?.languageService?.fillLeafsOnComplete ??
false,
},
);
}
public async getHoverInformation(
query: string,
position: IPosition,
filePath: Uri,
options?: HoverConfig,
): Promise<Hover['contents']> {
const projectConfig = this.getConfigForURI(filePath);
if (!projectConfig) {
return '';
}
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<DefinitionQueryResponse | null> {
const projectConfig = this.getConfigForURI(filePath);
if (!projectConfig) {
return null;
}
const schema = await this._graphQLCache.getSchema(projectConfig.name);
if (!schema) {
return null;
}
let ast;
try {
ast = parse(query);
} catch {
return null;
}
const node = getASTNodeAtPosition(query, ast, position);
// @ts-expect-error
const type = node && typeFromAST(schema, node);
let queryResult: DefinitionQueryResponse | null = null;
if (node) {
switch (node.kind) {
case Kind.FRAGMENT_SPREAD:
queryResult = await this._getDefinitionForFragmentSpread(
query,
ast,
node,
filePath,
projectConfig,
);
break;
case Kind.FRAGMENT_DEFINITION:
case Kind.OPERATION_DEFINITION:
queryResult = getDefinitionQueryResultForDefinitionNode(
filePath,
query,
node,
);
break;
case Kind.NAMED_TYPE:
queryResult = await this._getDefinitionForNamedType(
query,
ast,
node,
filePath,
projectConfig,
);
break;
case Kind.FIELD:
queryResult = await this._getDefinitionForField(
query,
ast,
node,
filePath,
projectConfig,
position,
);
break;
case Kind.ARGUMENT:
queryResult = await this._getDefinitionForArgument(
query,
ast,
node,
filePath,
projectConfig,
position,
);
break;
}
}
if (queryResult) {
return {
...queryResult,
node,
type,
};
}
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 ?? 'Anonymous',
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<DefinitionQueryResponse | null> {
const objectTypeDefinitions =
await this._graphQLCache.getObjectTypeDefinitions(projectConfig);
const dependencies =
await this._graphQLCache.getObjectTypeDependenciesForAST(
ast,
objectTypeDefinitions,
);
const localOperationDefinitionInfos = ast.definitions
.filter(isTypeDefinitionNode)
.map((definition: TypeDefinitionNode) => ({
filePath,
content: query,
definition,
}));
return getDefinitionQueryResultForNamedType(
query,
node,
dependencies.concat(localOperationDefinitionInfos),
);
}
async _getDefinitionForField(
query: string,
_ast: DocumentNode,
_node: FieldNode,
_filePath: Uri,
projectConfig: GraphQLProjectConfig,
position: IPosition,
) {
const token = getTokenAtPosition(query, position);
const schema = await this._graphQLCache.getSchema(projectConfig.name);
const typeInfo = getTypeInfo(schema!, token.state);
const fieldName = typeInfo.fieldDef?.name;
if (typeInfo && fieldName) {
const parentTypeName = (typeInfo.parentType as any).toString();
const objectTypeDefinitions =
await this._graphQLCache.getObjectTypeDefinitions(projectConfig);
// TODO: need something like getObjectTypeDependenciesForAST?
const dependencies = [...objectTypeDefinitions.values()];
return getDefinitionQueryResultForField(
fieldName,
parentTypeName,
dependencies,
);
}
return null;
}
async _getDefinitionForArgument(
query: string,
_ast: DocumentNode,
_node: ArgumentNode,
_filePath: Uri,
projectConfig: GraphQLProjectConfig,
position: IPosition,
) {
const token = getTokenAtPosition(query, position);
const schema = await this._graphQLCache.getSchema(projectConfig.name);
const typeInfo = getTypeInfo(schema!, token.state);
const fieldName = typeInfo.fieldDef?.name;
const argumentName = typeInfo.argDef?.name;
if (typeInfo && fieldName && argumentName) {
const objectTypeDefinitions =
await this._graphQLCache.getObjectTypeDefinitions(projectConfig);
// TODO: need something like getObjectTypeDependenciesForAST?
const dependencies = [...objectTypeDefinitions.values()];
return getDefinitionQueryResultForArgument(
argumentName,
fieldName,
// @ts-expect-error - typeInfo is not typed correctly
typeInfo.argDef?.type?.name,
dependencies,
);
}
return null;
}
async _getDefinitionForFragmentSpread(
query: string,
ast: DocumentNode,
node: FragmentSpreadNode,
filePath: Uri,
projectConfig: GraphQLProjectConfig,
): Promise<DefinitionQueryResponse | null> {
const fragmentDefinitions =
await this._graphQLCache.getFragmentDefinitions(projectConfig);
const dependencies = await this._graphQLCache.getFragmentDependenciesForAST(
ast,
fragmentDefinitions,
);
const localFragDefinitions = ast.definitions.filter(
definition => definition.kind === Kind.FRAGMENT_DEFINITION,
);
const typeCastedDefs =
localFragDefinitions as any as Array<FragmentDefinitionNode>;
const localFragInfos = typeCastedDefs.map(
(definition: FragmentDefinitionNode) => ({
filePath,
content: query,
definition,
}),
);
return getDefinitionQueryResultForFragmentSpread(
query,
node,
dependencies.concat(localFragInfos),
);
}
async getOutline(documentText: string): Promise<Outline | null> {
return getOutline(documentText);
}
}