Skip to content
This repository was archived by the owner on Sep 2, 2020. It is now read-only.

Commit cc6b063

Browse files
Divyendu Singhasiandrummer
authored andcommitted
Feature go to definition SDL - input, enum, type (#237)
* feature: add go to definition for named types: input, enum, type * fix: flow types * fix: more flow types * fix: flow errors * feature: make go to definition live * docs: combine fragment, named types in readme * use: npm run pretty
1 parent 068c57f commit cc6b063

File tree

11 files changed

+479
-6
lines changed

11 files changed

+479
-6
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Partial support for [Microsoft's Language Server Protocol](https://github.com/Mi
1313
Currently supported features include:
1414
- Diagnostics (GraphQL syntax linting/validations) (**spec-compliant**)
1515
- Autocomplete suggestions (**spec-compliant**)
16-
- Hyperlink to fragment definitions (**spec-compliant**)
16+
- Hyperlink to fragment definitions and named types (type, input, enum) definitions (**spec-compliant**)
1717
- Outline view support for queries
1818

1919

packages/interface/src/GraphQLLanguageService.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import type {
1313
FragmentSpreadNode,
1414
FragmentDefinitionNode,
1515
OperationDefinitionNode,
16+
TypeDefinitionNode,
17+
NamedTypeNode,
1618
} from 'graphql';
1719
import type {
1820
CompletionItem,
@@ -43,6 +45,7 @@ import {
4345
DIRECTIVE_DEFINITION,
4446
FRAGMENT_SPREAD,
4547
OPERATION_DEFINITION,
48+
NAMED_TYPE,
4649
} from 'graphql/language/kinds';
4750

4851
import {parse, print} from 'graphql';
@@ -52,6 +55,7 @@ import {validateQuery, getRange, SEVERITY} from './getDiagnostics';
5255
import {
5356
getDefinitionQueryResultForFragmentSpread,
5457
getDefinitionQueryResultForDefinitionNode,
58+
getDefinitionQueryResultForNamedType,
5559
} from './getDefinition';
5660
import {getASTNodeAtPosition} from 'graphql-language-service-utils';
5761

@@ -224,11 +228,63 @@ export class GraphQLLanguageService {
224228
query,
225229
(node: FragmentDefinitionNode | OperationDefinitionNode),
226230
);
231+
case NAMED_TYPE:
232+
return this._getDefinitionForNamedType(
233+
query,
234+
ast,
235+
node,
236+
filePath,
237+
projectConfig,
238+
);
227239
}
228240
}
229241
return null;
230242
}
231243

244+
async _getDefinitionForNamedType(
245+
query: string,
246+
ast: DocumentNode,
247+
node: NamedTypeNode,
248+
filePath: Uri,
249+
projectConfig: GraphQLProjectConfig,
250+
): Promise<?DefinitionQueryResult> {
251+
const objectTypeDefinitions = await this._graphQLCache.getObjectTypeDefinitions(
252+
projectConfig,
253+
);
254+
255+
const dependencies = await this._graphQLCache.getObjectTypeDependenciesForAST(
256+
ast,
257+
objectTypeDefinitions,
258+
);
259+
260+
const localObjectTypeDefinitions = ast.definitions.filter(
261+
definition =>
262+
definition.kind === OBJECT_TYPE_DEFINITION ||
263+
definition.kind === INPUT_OBJECT_TYPE_DEFINITION ||
264+
definition.kind === ENUM_TYPE_DEFINITION,
265+
);
266+
267+
const typeCastedDefs = ((localObjectTypeDefinitions: any): Array<
268+
TypeDefinitionNode,
269+
>);
270+
271+
const localOperationDefinationInfos = typeCastedDefs.map(
272+
(definition: TypeDefinitionNode) => ({
273+
filePath,
274+
content: query,
275+
definition,
276+
}),
277+
);
278+
279+
const result = await getDefinitionQueryResultForNamedType(
280+
query,
281+
node,
282+
dependencies.concat(localOperationDefinationInfos),
283+
);
284+
285+
return result;
286+
}
287+
232288
async _getDefinitionForFragmentSpread(
233289
query: string,
234290
ast: DocumentNode,

packages/interface/src/__tests__/GraphQLLanguageService-test.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,51 @@ import {GraphQLConfig} from 'graphql-config';
1616
import {GraphQLLanguageService} from '../GraphQLLanguageService';
1717

1818
const MOCK_CONFIG = {
19-
includes: ['./queries/**'],
19+
schemaPath: './__schema__/StarWarsSchema.graphql',
20+
includes: ['./queries/**', '**/*.graphql'],
2021
};
2122

2223
describe('GraphQLLanguageService', () => {
2324
const mockCache: any = {
2425
getGraphQLConfig() {
2526
return new GraphQLConfig(MOCK_CONFIG, join(__dirname, '.graphqlconfig'));
2627
},
28+
29+
getObjectTypeDefinitions() {
30+
return {
31+
Episode: {
32+
filePath: 'fake file path',
33+
content: 'fake file content',
34+
definition: {
35+
name: {
36+
value: 'Episode',
37+
},
38+
loc: {
39+
start: 293,
40+
end: 335,
41+
},
42+
},
43+
},
44+
};
45+
},
46+
47+
getObjectTypeDependenciesForAST() {
48+
return [
49+
{
50+
filePath: 'fake file path',
51+
content: 'fake file content',
52+
definition: {
53+
name: {
54+
value: 'Episode',
55+
},
56+
loc: {
57+
start: 293,
58+
end: 335,
59+
},
60+
},
61+
},
62+
];
63+
},
2764
};
2865

2966
let languageService;
@@ -42,4 +79,13 @@ describe('GraphQLLanguageService', () => {
4279
'Syntax Error: Unexpected Name "qeury"',
4380
);
4481
});
82+
83+
it('runs definition service as expected', async () => {
84+
const definitionQueryResult = await languageService.getDefinition(
85+
'type Query { hero(episode: Episode): Character }',
86+
{line: 0, character: 28},
87+
'./queries/definitionQuery.graphql',
88+
);
89+
expect(definitionQueryResult.definitions.length).to.equal(1);
90+
});
4591
});

packages/interface/src/__tests__/getDefinition-test.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,46 @@
1111
import {expect} from 'chai';
1212
import {describe, it} from 'mocha';
1313
import {parse} from 'graphql';
14-
import {getDefinitionQueryResultForFragmentSpread} from '../getDefinition';
14+
import {
15+
getDefinitionQueryResultForFragmentSpread,
16+
getDefinitionQueryResultForNamedType,
17+
} from '../getDefinition';
1518

1619
describe('getDefinition', () => {
20+
describe('getDefinitionQueryResultForNamedType', () => {
21+
it('returns correct Position', async () => {
22+
const query = `type Query {
23+
hero(episode: Episode): Character
24+
}
25+
26+
type Episode {
27+
id: ID!
28+
}
29+
`;
30+
const parsedQuery = parse(query);
31+
const namedTypeDefinition = parsedQuery.definitions[0].fields[0].type;
32+
33+
const result = await getDefinitionQueryResultForNamedType(
34+
query,
35+
{
36+
...namedTypeDefinition,
37+
},
38+
[
39+
{
40+
file: 'someFile',
41+
content: query,
42+
definition: {
43+
...namedTypeDefinition,
44+
},
45+
},
46+
],
47+
);
48+
expect(result.definitions.length).to.equal(1);
49+
expect(result.definitions[0].position.line).to.equal(1);
50+
expect(result.definitions[0].position.character).to.equal(32);
51+
});
52+
});
53+
1754
describe('getDefinitionQueryResultForFragmentSpread', () => {
1855
it('returns correct Position', async () => {
1956
const query = `query A {
@@ -39,7 +76,7 @@ describe('getDefinition', () => {
3976
);
4077
expect(result.definitions.length).to.equal(1);
4178
expect(result.definitions[0].position.line).to.equal(1);
42-
expect(result.definitions[0].position.character).to.equal(15);
79+
expect(result.definitions[0].position.character).to.equal(6);
4380
});
4481
});
4582
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
type Query { hero(episode: Episode): Character }

packages/interface/src/getDefinition.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import type {
1313
FragmentSpreadNode,
1414
FragmentDefinitionNode,
1515
OperationDefinitionNode,
16+
NamedTypeNode,
17+
TypeDefinitionNode,
1618
} from 'graphql';
1719
import type {
1820
Definition,
@@ -21,6 +23,7 @@ import type {
2123
Position,
2224
Range,
2325
Uri,
26+
ObjectTypeInfo,
2427
} from 'graphql-language-service-types';
2528
import {locToRange, offsetToPosition} from 'graphql-language-service-utils';
2629
import invariant from 'assert';
@@ -39,6 +42,29 @@ function getPosition(text: string, node: ASTNode): Position {
3942
return offsetToPosition(text, location.start);
4043
}
4144

45+
export async function getDefinitionQueryResultForNamedType(
46+
text: string,
47+
node: NamedTypeNode,
48+
dependencies: Array<ObjectTypeInfo>,
49+
): Promise<DefinitionQueryResult> {
50+
const name = node.name.value;
51+
const defNodes = dependencies.filter(
52+
({definition}) => definition.name && definition.name.value === name,
53+
);
54+
if (defNodes.length === 0) {
55+
process.stderr.write(`Definition not found for GraphQL type ${name}`);
56+
return {queryRange: [], definitions: []};
57+
}
58+
const definitions: Array<Definition> = defNodes.map(
59+
({filePath, content, definition}) =>
60+
getDefinitionForNodeDefinition(filePath || '', content, definition),
61+
);
62+
return {
63+
definitions,
64+
queryRange: definitions.map(_ => getRange(text, node)),
65+
};
66+
}
67+
4268
export async function getDefinitionQueryResultForFragmentSpread(
4369
text: string,
4470
fragment: FragmentSpreadNode,
@@ -82,7 +108,25 @@ function getDefinitionForFragmentDefinition(
82108
invariant(name, 'Expected ASTNode to have a Name.');
83109
return {
84110
path,
85-
position: getPosition(text, name),
111+
position: getPosition(text, definition),
112+
range: getRange(text, definition),
113+
name: name.value || '',
114+
language: LANGUAGE,
115+
// This is a file inside the project root, good enough for now
116+
projectRoot: path,
117+
};
118+
}
119+
120+
function getDefinitionForNodeDefinition(
121+
path: Uri,
122+
text: string,
123+
definition: TypeDefinitionNode,
124+
): Definition {
125+
const name = definition.name;
126+
invariant(name, 'Expected ASTNode to have a Name.');
127+
return {
128+
path,
129+
position: getPosition(text, definition),
86130
range: getRange(text, definition),
87131
name: name.value || '',
88132
language: LANGUAGE,

0 commit comments

Comments
 (0)