Skip to content

Commit

Permalink
feature: add definition support for operation fields (#2486)
Browse files Browse the repository at this point in the history
* add definition support for operation fields

* add changeset

Co-authored-by: Rikki Schulte <rikki.schulte@gmail.com>
  • Loading branch information
stonexer and acao authored Jun 9, 2022
1 parent dec8bec commit c9c51b8
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 12 deletions.
9 changes: 9 additions & 0 deletions .changeset/itchy-bananas-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"graphql-language-service-cli": patch
"graphql-language-service-server": patch
"graphql-language-service": patch
---

definition support for operation fields ✨

you can now jump to the applicable object type definition for query/mutation/subscription fields!
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TypeDefinitionNode,
NamedTypeNode,
ValidationRule,
FieldNode,
} from 'graphql';

import {
Expand All @@ -34,8 +35,11 @@ import {
getDefinitionQueryResultForFragmentSpread,
getDefinitionQueryResultForDefinitionNode,
getDefinitionQueryResultForNamedType,
getDefinitionQueryResultForField,
DefinitionQueryResult,
getASTNodeAtPosition,
getTokenAtPosition,
getTypeInfo,
} from 'graphql-language-service';

import { GraphQLConfig, GraphQLProjectConfig } from 'graphql-config';
Expand All @@ -47,6 +51,7 @@ import {
} from 'vscode-languageserver-types';

import { Kind, parse, print } from 'graphql';
import { Logger } from './Logger';

const {
FRAGMENT_DEFINITION,
Expand All @@ -66,6 +71,7 @@ const {
FRAGMENT_SPREAD,
OPERATION_DEFINITION,
NAMED_TYPE,
FIELD,
} = Kind;

const KIND_TO_SYMBOL_KIND: { [key: string]: SymbolKind } = {
Expand Down Expand Up @@ -99,10 +105,13 @@ function getKind(tree: OutlineTree) {
export class GraphQLLanguageService {
_graphQLCache: GraphQLCache;
_graphQLConfig: GraphQLConfig;
_logger: Logger;

constructor(cache: GraphQLCache) {
constructor(cache: GraphQLCache, logger: Logger) {
this._graphQLCache = cache;
this._graphQLConfig = cache.getGraphQLConfig();

this._logger = logger;
}

getConfigForURI(uri: Uri) {
Expand Down Expand Up @@ -152,7 +161,8 @@ export class GraphQLLanguageService {
return false;
});
}
} catch (error) {
} catch (err) {
const error = err as any;
const range = getRange(error.locations[0], document);
return [
{
Expand Down Expand Up @@ -304,6 +314,16 @@ export class GraphQLLanguageService {
filePath,
projectConfig,
);

case FIELD:
return this._getDefinitionForField(
query,
ast,
node,
filePath,
projectConfig,
position,
);
}
}
return null;
Expand Down Expand Up @@ -404,6 +424,42 @@ export class GraphQLLanguageService {
return result;
}

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()];

const result = await getDefinitionQueryResultForField(
fieldName,
parentTypeName,
dependencies,
);

return result;
}

return null;
}

async _getDefinitionForFragmentSpread(
query: string,
ast: DocumentNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,10 @@ export class MessageProcessor {

logger: this._logger,
});
this._languageService = new GraphQLLanguageService(this._graphQLCache);
this._languageService = new GraphQLLanguageService(
this._graphQLCache,
this._logger,
);
if (this._graphQLConfig || this._graphQLCache?.getGraphQLConfig) {
const config =
this._graphQLConfig ?? this._graphQLCache.getGraphQLConfig();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { GraphQLConfig } from 'graphql-config';
import { GraphQLLanguageService } from '../GraphQLLanguageService';
import { SymbolKind } from 'vscode-languageserver-protocol';
import { Position } from 'graphql-language-service';
import { Logger } from '../Logger';

const MOCK_CONFIG = {
filepath: join(__dirname, '.graphqlrc.yml'),
Expand All @@ -38,8 +39,54 @@ describe('GraphQLLanguageService', () => {
},

getObjectTypeDefinitions() {
return {
Episode: {
const definitions = new Map();

definitions.set('Episode', {
filePath: 'fake file path',
content: 'fake file content',
definition: {
name: {
value: 'Episode',
},

loc: {
start: 293,
end: 335,
},
},
});

definitions.set('Human', {
filePath: 'fake file path',
content: 'fake file content',
definition: {
name: {
value: 'Human',
},

fields: [
{
name: { value: 'name' },
loc: {
start: 293,
end: 335,
},
},
],

loc: {
start: 293,
end: 335,
},
},
});

return definitions;
},

getObjectTypeDependenciesForAST() {
return [
{
filePath: 'fake file path',
content: 'fake file content',
definition: {
Expand All @@ -53,17 +100,12 @@ describe('GraphQLLanguageService', () => {
},
},
},
};
},

getObjectTypeDependenciesForAST() {
return [
{
filePath: 'fake file path',
content: 'fake file content',
definition: {
name: {
value: 'Episode',
value: 'Human',
},

loc: {
Expand All @@ -78,7 +120,10 @@ describe('GraphQLLanguageService', () => {

let languageService: GraphQLLanguageService;
beforeEach(() => {
languageService = new GraphQLLanguageService(mockCache as any);
languageService = new GraphQLLanguageService(
mockCache as any,
new Logger(),
);
});

it('runs diagnostic service as expected', async () => {
Expand Down Expand Up @@ -122,6 +167,16 @@ describe('GraphQLLanguageService', () => {
expect(definitionQueryResult.definitions.length).toEqual(1);
});

it('runs definition service on field as expected', async () => {
const definitionQueryResult = await languageService.getDefinition(
'query XXX { human { name } }',
{ line: 0, character: 21 } as Position,
'./queries/definitionQuery.graphql',
);
// @ts-ignore
expect(definitionQueryResult.definitions.length).toEqual(1);
});

it('runs hover service as expected', async () => {
const hoverInformation = await languageService.getHoverInformation(
'type Query { hero(episode: String): String }',
Expand Down
1 change: 1 addition & 0 deletions packages/graphql-language-service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
getDefinitionQueryResultForDefinitionNode,
getDefinitionQueryResultForFragmentSpread,
getDefinitionQueryResultForNamedType,
getDefinitionQueryResultForField,
getDefinitionState,
getDiagnostics,
getFieldDef,
Expand Down
56 changes: 56 additions & 0 deletions packages/graphql-language-service/src/interface/getDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
NamedTypeNode,
TypeDefinitionNode,
Location,
ObjectTypeDefinitionNode,
FieldDefinitionNode,
} from 'graphql';

import { Definition, FragmentInfo, Uri, ObjectTypeInfo } from '../types';
Expand Down Expand Up @@ -70,6 +72,42 @@ export async function getDefinitionQueryResultForNamedType(
};
}

export async function getDefinitionQueryResultForField(
fieldName: string,
typeName: string,
dependencies: Array<ObjectTypeInfo>,
): Promise<DefinitionQueryResult> {
const defNodes = dependencies.filter(
({ definition }) => definition.name && definition.name.value === typeName,
);

if (defNodes.length === 0) {
throw Error(`Definition not found for GraphQL type ${typeName}`);
}

const definitions: Array<Definition> = [];

defNodes.forEach(({ filePath, content, definition }) => {
const fieldDefinition = (definition as ObjectTypeDefinitionNode).fields?.find(
item => item.name.value === fieldName,
);

if (fieldDefinition == null) {
return null;
}

definitions.push(
getDefinitionForFieldDefinition(filePath || '', content, fieldDefinition),
);
});

return {
definitions,
// TODO: seems like it's not using
queryRange: [],
};
}

export async function getDefinitionQueryResultForFragmentSpread(
text: string,
fragment: FragmentSpreadNode,
Expand Down Expand Up @@ -145,3 +183,21 @@ function getDefinitionForNodeDefinition(
projectRoot: path,
};
}

function getDefinitionForFieldDefinition(
path: Uri,
text: string,
definition: FieldDefinitionNode,
): Definition {
const name = definition.name;
assert(name, 'Expected ASTNode to have a Name.');
return {
path,
position: getPosition(text, definition),
range: getRange(text, definition),
name: name.value || '',
language: LANGUAGE,
// This is a file inside the project root, good enough for now
projectRoot: path,
};
}

0 comments on commit c9c51b8

Please sign in to comment.