diff --git a/README.md b/README.md index fb42de09..2673481b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Currently supported features include: - Diagnostics (GraphQL syntax linting/validations) (**spec-compliant**) - Autocomplete suggestions (**spec-compliant**) - Hyperlink to fragment definitions (**spec-compliant**) +- Hyperlink to named types (type, input, enum) definitions (**spec-compliant**) - Outline view support for queries diff --git a/package.json b/package.json index 8a55c6c4..454a53bd 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,6 @@ "graphql-language-service-utils": "^1.0.0-0", "lerna": "^2.0.0", "mocha": "4.1.0", - "prettier": "^1.5.3" + "prettier": "1.13" } } diff --git a/packages/interface/src/GraphQLLanguageService.js b/packages/interface/src/GraphQLLanguageService.js index ac2430e8..b262db30 100644 --- a/packages/interface/src/GraphQLLanguageService.js +++ b/packages/interface/src/GraphQLLanguageService.js @@ -13,6 +13,8 @@ import type { FragmentSpreadNode, FragmentDefinitionNode, OperationDefinitionNode, + TypeDefinitionNode, + NamedTypeNode, } from 'graphql'; import type { CompletionItem, @@ -43,6 +45,7 @@ import { DIRECTIVE_DEFINITION, FRAGMENT_SPREAD, OPERATION_DEFINITION, + NAMED_TYPE, } from 'graphql/language/kinds'; import {parse, print} from 'graphql'; @@ -52,6 +55,7 @@ import {validateQuery, getRange, SEVERITY} from './getDiagnostics'; import { getDefinitionQueryResultForFragmentSpread, getDefinitionQueryResultForDefinitionNode, + getDefinitionQueryResultForNamedType, } from './getDefinition'; import {getASTNodeAtPosition} from 'graphql-language-service-utils'; @@ -224,11 +228,63 @@ export class GraphQLLanguageService { query, (node: FragmentDefinitionNode | OperationDefinitionNode), ); + case NAMED_TYPE: + return this._getDefinitionForNamedType( + query, + ast, + node, + filePath, + projectConfig, + ); } } return null; } + async _getDefinitionForNamedType( + query: string, + ast: DocumentNode, + node: NamedTypeNode, + filePath: Uri, + projectConfig: GraphQLProjectConfig, + ): Promise { + 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, + ); + + const typeCastedDefs = ((localObjectTypeDefinitions: any): 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, diff --git a/packages/interface/src/__tests__/GraphQLLanguageService-test.js b/packages/interface/src/__tests__/GraphQLLanguageService-test.js index b5f65738..ee1773a3 100644 --- a/packages/interface/src/__tests__/GraphQLLanguageService-test.js +++ b/packages/interface/src/__tests__/GraphQLLanguageService-test.js @@ -16,7 +16,8 @@ import {GraphQLConfig} from 'graphql-config'; import {GraphQLLanguageService} from '../GraphQLLanguageService'; const MOCK_CONFIG = { - includes: ['./queries/**'], + schemaPath: './__schema__/StarWarsSchema.graphql', + includes: ['./queries/**', '**/*.graphql'], }; describe('GraphQLLanguageService', () => { @@ -24,6 +25,42 @@ describe('GraphQLLanguageService', () => { getGraphQLConfig() { return new GraphQLConfig(MOCK_CONFIG, join(__dirname, '.graphqlconfig')); }, + + getObjectTypeDefinitions() { + return { + Episode: { + filePath: 'fake file path', + content: 'fake file content', + definition: { + name: { + value: 'Episode', + }, + loc: { + start: 293, + end: 335, + }, + }, + }, + }; + }, + + getObjectTypeDependenciesForAST() { + return [ + { + filePath: 'fake file path', + content: 'fake file content', + definition: { + name: { + value: 'Episode', + }, + loc: { + start: 293, + end: 335, + }, + }, + }, + ]; + }, }; let languageService; @@ -33,9 +70,18 @@ describe('GraphQLLanguageService', () => { it('runs diagnostic service as expected', async () => { const diagnostics = await languageService.getDiagnostics( - 'qeury', + 'query', './queries/testQuery.graphql', ); expect(diagnostics.length).to.equal(1); }); + + it('runs definition service as expected', async () => { + const definitionQueryResult = await languageService.getDefinition( + 'type Query { hero(episode: Episode): Character }', + {line: 0, character: 28}, + './queries/definitionQuery.graphql', + ); + expect(definitionQueryResult.definitions.length).to.equal(1); + }); }); diff --git a/packages/interface/src/__tests__/getDefinition-test.js b/packages/interface/src/__tests__/getDefinition-test.js index e359d65f..5a5c4069 100644 --- a/packages/interface/src/__tests__/getDefinition-test.js +++ b/packages/interface/src/__tests__/getDefinition-test.js @@ -11,9 +11,46 @@ import {expect} from 'chai'; import {describe, it} from 'mocha'; import {parse} from 'graphql'; -import {getDefinitionQueryResultForFragmentSpread} from '../getDefinition'; +import { + getDefinitionQueryResultForFragmentSpread, + getDefinitionQueryResultForNamedType, +} from '../getDefinition'; describe('getDefinition', () => { + describe('getDefinitionQueryResultForNamedType', () => { + it('returns correct Position', async () => { + const query = `type Query { + hero(episode: Episode): Character + } + + type Episode { + id: ID! + } + `; + const parsedQuery = parse(query); + const namedTypeDefinition = parsedQuery.definitions[0].fields[0].type; + + const result = await getDefinitionQueryResultForNamedType( + query, + { + ...namedTypeDefinition, + }, + [ + { + file: 'someFile', + content: query, + definition: { + ...namedTypeDefinition, + }, + }, + ], + ); + expect(result.definitions.length).to.equal(1); + expect(result.definitions[0].position.line).to.equal(1); + expect(result.definitions[0].position.character).to.equal(32); + }); + }); + describe('getDefinitionQueryResultForFragmentSpread', () => { it('returns correct Position', async () => { const query = `query A { @@ -39,7 +76,7 @@ describe('getDefinition', () => { ); expect(result.definitions.length).to.equal(1); expect(result.definitions[0].position.line).to.equal(1); - expect(result.definitions[0].position.character).to.equal(15); + expect(result.definitions[0].position.character).to.equal(6); }); }); }); diff --git a/packages/interface/src/__tests__/getDiagnostics-test.js b/packages/interface/src/__tests__/getDiagnostics-test.js index 0014acae..f3d16e45 100644 --- a/packages/interface/src/__tests__/getDiagnostics-test.js +++ b/packages/interface/src/__tests__/getDiagnostics-test.js @@ -47,6 +47,41 @@ describe('getDiagnostics', () => { expect(error.source).to.equal('GraphQL: Deprecation'); }); + it('returns no errors for valid query', () => { + const errors = getDiagnostics('query { hero { name } }', schema); + expect(errors.length).to.equal(0); + }); + + it('returns no errors for valid query', () => { + const errors = getDiagnostics('query { hero { name } }', schema); + expect(errors.length).to.equal(0); + }); + + it('returns no errors for valid query with aliases', () => { + const errors = getDiagnostics( + 'query { superHero: hero { superName :name } }', + schema, + ); + expect(errors.length).to.equal(0); + }); + + it('catches a syntax error in the SDL', () => { + const errors = getDiagnostics( + ` + type Human implements Character { + SyntaxError + id: String! + } + `, + schema, + ); + expect(errors.length).to.equal(1); + const error = errors[0]; + expect(error.message).to.equal('Syntax Error: Expected :, found Name "id"'); + expect(error.severity).to.equal(SEVERITY.ERROR); + expect(error.source).to.equal('GraphQL: Syntax'); + }); + // TODO: change this kitchen sink to depend on the local schema // and then run diagnostics with the schema it('returns no errors after parsing kitchen-sink query', () => { diff --git a/packages/interface/src/__tests__/queries/definitionQuery.graphql b/packages/interface/src/__tests__/queries/definitionQuery.graphql new file mode 100644 index 00000000..a1028da7 --- /dev/null +++ b/packages/interface/src/__tests__/queries/definitionQuery.graphql @@ -0,0 +1 @@ +type Query { hero(episode: Episode): Character } \ No newline at end of file diff --git a/packages/interface/src/getAutocompleteSuggestions.js b/packages/interface/src/getAutocompleteSuggestions.js index 3dab6e78..5efd0bd2 100644 --- a/packages/interface/src/getAutocompleteSuggestions.js +++ b/packages/interface/src/getAutocompleteSuggestions.js @@ -304,8 +304,9 @@ function getSuggestionsForFragmentSpread( relevantFrags.map(frag => ({ label: frag.name.value, detail: String(typeMap[frag.typeCondition.name.value]), - documentation: `fragment ${frag.name.value} on ${frag.typeCondition.name - .value}`, + documentation: `fragment ${frag.name.value} on ${ + frag.typeCondition.name.value + }`, })), ); } diff --git a/packages/interface/src/getDefinition.js b/packages/interface/src/getDefinition.js index 2a516b62..aff0af3e 100644 --- a/packages/interface/src/getDefinition.js +++ b/packages/interface/src/getDefinition.js @@ -13,6 +13,8 @@ import type { FragmentSpreadNode, FragmentDefinitionNode, OperationDefinitionNode, + NamedTypeNode, + TypeDefinitionNode, } from 'graphql'; import type { Definition, @@ -21,6 +23,7 @@ import type { Position, Range, Uri, + ObjectTypeInfo, } from 'graphql-language-service-types'; import {locToRange, offsetToPosition} from 'graphql-language-service-utils'; import invariant from 'assert'; @@ -39,6 +42,29 @@ function getPosition(text: string, node: ASTNode): Position { return offsetToPosition(text, location.start); } +export async function getDefinitionQueryResultForNamedType( + text: string, + node: NamedTypeNode, + dependencies: Array, +): Promise { + const name = node.name.value; + const defNodes = dependencies.filter( + ({definition}) => definition.name && definition.name.value === name, + ); + if (defNodes.length === 0) { + process.stderr.write(`Definition not found for GraphQL type ${name}`); + return {queryRange: [], definitions: []}; + } + const definitions: Array = defNodes.map( + ({filePath, content, definition}) => + getDefinitionForNodeDefinition(filePath || '', content, definition), + ); + return { + definitions, + queryRange: definitions.map(_ => getRange(text, node)), + }; +} + export async function getDefinitionQueryResultForFragmentSpread( text: string, fragment: FragmentSpreadNode, @@ -52,10 +78,9 @@ export async function getDefinitionQueryResultForFragmentSpread( process.stderr.write(`Definition not found for GraphQL fragment ${name}`); return {queryRange: [], definitions: []}; } - const definitions: Array< - Definition, - > = defNodes.map(({filePath, content, definition}) => - getDefinitionForFragmentDefinition(filePath || '', content, definition), + const definitions: Array = defNodes.map( + ({filePath, content, definition}) => + getDefinitionForFragmentDefinition(filePath || '', content, definition), ); return { definitions, @@ -83,7 +108,25 @@ function getDefinitionForFragmentDefinition( invariant(name, 'Expected ASTNode to have a Name.'); return { path, - position: getPosition(text, name), + 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, + }; +} + +function getDefinitionForNodeDefinition( + path: Uri, + text: string, + definition: TypeDefinitionNode, +): Definition { + const name = definition.name; + invariant(name, 'Expected ASTNode to have a Name.'); + return { + path, + position: getPosition(text, definition), range: getRange(text, definition), name: name.value || '', language: LANGUAGE, diff --git a/packages/interface/src/getDiagnostics.js b/packages/interface/src/getDiagnostics.js index a89d9b04..b052221f 100644 --- a/packages/interface/src/getDiagnostics.js +++ b/packages/interface/src/getDiagnostics.js @@ -106,7 +106,9 @@ function annotations( const highlightNode = node.kind !== 'Variable' && node.name ? node.name - : node.variable ? node.variable : node; + : node.variable + ? node.variable + : node; invariant(error.locations, 'GraphQL validation error requires locations.'); const loc = error.locations[0]; diff --git a/packages/parser/src/Rules.js b/packages/parser/src/Rules.js index f9479524..cb33f089 100644 --- a/packages/parser/src/Rules.js +++ b/packages/parser/src/Rules.js @@ -18,8 +18,8 @@ import type { import {opt, list, butNot, t, p} from './RuleHelpers'; /** - * Whitespace tokens defined in GraphQL spec. - */ + * Whitespace tokens defined in GraphQL spec. + */ export const isIgnored = (ch: string) => ch === ' ' || ch === '\t' || @@ -120,7 +120,9 @@ export const ParseRules: {[name: string]: ParseRule} = { ? stream.match(/[\s\u00a0,]*(on\b|@|{)/, false) ? 'InlineFragment' : 'FragmentSpread' - : stream.match(/[\s\u00a0,]*:/, false) ? 'AliasedField' : 'Field'; + : stream.match(/[\s\u00a0,]*:/, false) + ? 'AliasedField' + : 'Field'; }, // Note: this minor deviation of "AliasedField" simplifies the lookahead. AliasedField: [ diff --git a/packages/parser/src/onlineParser.js b/packages/parser/src/onlineParser.js index cfb1c2a5..c88805f2 100644 --- a/packages/parser/src/onlineParser.js +++ b/packages/parser/src/onlineParser.js @@ -149,7 +149,9 @@ function getToken( // the current token, otherwise expect based on the current step. let expected: any = typeof state.rule === 'function' - ? state.step === 0 ? state.rule(token, stream) : null + ? state.step === 0 + ? state.rule(token, stream) + : null : state.rule[state.step]; // Seperator between list elements if necessary. diff --git a/packages/server/src/GraphQLCache.js b/packages/server/src/GraphQLCache.js index a42ad09b..111ca97f 100644 --- a/packages/server/src/GraphQLCache.js +++ b/packages/server/src/GraphQLCache.js @@ -16,6 +16,7 @@ import type { GraphQLFileMetadata, GraphQLFileInfo, FragmentInfo, + ObjectTypeInfo, Uri, GraphQLProjectConfig, } from 'graphql-language-service-types'; @@ -65,6 +66,7 @@ export class GraphQLCache implements GraphQLCacheInterface { _schemaMap: Map; _typeExtensionMap: Map; _fragmentDefinitionsCache: Map>; + _typeDefinitionsCache: Map>; constructor(configDir: Uri, graphQLConfig: GraphQLConfig): void { this._configDir = configDir; @@ -72,6 +74,7 @@ export class GraphQLCache implements GraphQLCacheInterface { this._graphQLFileListCache = new Map(); this._schemaMap = new Map(); this._fragmentDefinitionsCache = new Map(); + this._typeDefinitionsCache = new Map(); this._typeExtensionMap = new Map(); } @@ -177,6 +180,112 @@ export class GraphQLCache implements GraphQLCacheInterface { return fragmentDefinitions; }; + getObjectTypeDependencies = async ( + query: string, + objectTypeDefinitions: ?Map, + ): Promise> => { + // If there isn't context for object type references, + // return an empty array. + if (!objectTypeDefinitions) { + return []; + } + // If the query cannot be parsed, validations cannot happen yet. + // Return an empty array. + let parsedQuery; + try { + parsedQuery = parse(query); + } catch (error) { + return []; + } + return this.getObjectTypeDependenciesForAST( + parsedQuery, + objectTypeDefinitions, + ); + }; + + getObjectTypeDependenciesForAST = async ( + parsedQuery: ASTNode, + objectTypeDefinitions: Map, + ): Promise> => { + if (!objectTypeDefinitions) { + return []; + } + + const existingObjectTypes = new Map(); + const referencedObjectTypes = new Set(); + + visit(parsedQuery, { + ObjectTypeDefinition(node) { + existingObjectTypes.set(node.name.value, true); + }, + InputObjectTypeDefinition(node) { + existingObjectTypes.set(node.name.value, true); + }, + EnumTypeDefinition(node) { + existingObjectTypes.set(node.name.value, true); + }, + NamedType(node) { + if (!referencedObjectTypes.has(node.name.value)) { + referencedObjectTypes.add(node.name.value); + } + }, + }); + + const asts = new Set(); + referencedObjectTypes.forEach(name => { + if (!existingObjectTypes.has(name) && objectTypeDefinitions.has(name)) { + asts.add(nullthrows(objectTypeDefinitions.get(name))); + } + }); + + const referencedObjects = []; + + asts.forEach(ast => { + visit(ast.definition, { + NamedType(node) { + if ( + !referencedObjectTypes.has(node.name.value) && + objectTypeDefinitions.get(node.name.value) + ) { + asts.add(nullthrows(objectTypeDefinitions.get(node.name.value))); + referencedObjectTypes.add(node.name.value); + } + }, + }); + if (!existingObjectTypes.has(ast.definition.name.value)) { + referencedObjects.push(ast); + } + }); + + return referencedObjects; + }; + + getObjectTypeDefinitions = async ( + projectConfig: GraphQLProjectConfig, + ): Promise> => { + // This function may be called from other classes. + // If then, check the cache first. + const rootDir = projectConfig.configDir; + if (this._typeDefinitionsCache.has(rootDir)) { + return this._typeDefinitionsCache.get(rootDir) || new Map(); + } + const filesFromInputDirs = await this._readFilesFromInputDirs( + rootDir, + projectConfig.includes, + ); + const list = filesFromInputDirs.filter(fileInfo => + projectConfig.includesFile(fileInfo.filePath), + ); + const { + objectTypeDefinitions, + graphQLFileMap, + } = await this.readAllGraphQLFiles(list); + this._typeDefinitionsCache.set(rootDir, objectTypeDefinitions); + this._graphQLFileListCache.set(rootDir, graphQLFileMap); + + return objectTypeDefinitions; + }; + handleWatchmanSubscribeEvent = ( rootDir: string, projectConfig: GraphQLProjectConfig, @@ -233,6 +342,7 @@ export class GraphQLCache implements GraphQLCacheInterface { } this.updateFragmentDefinitionCache(rootDir, filePath, exists); + this.updateObjectTypeDefinitionCache(rootDir, filePath, exists); } }); } @@ -383,6 +493,73 @@ export class GraphQLCache implements GraphQLCacheInterface { } } + async updateObjectTypeDefinition( + rootDir: Uri, + filePath: Uri, + contents: Array, + ): Promise { + const cache = this._typeDefinitionsCache.get(rootDir); + const asts = contents.map(({query}) => { + try { + return {ast: parse(query), query}; + } catch (error) { + return {ast: null, query}; + } + }); + if (cache) { + // first go through the types list to delete the ones from this file + cache.forEach((value, key) => { + if (value.filePath === filePath) { + cache.delete(key); + } + }); + asts.forEach(({ast, query}) => { + if (!ast) { + return; + } + ast.definitions.forEach(definition => { + if ( + definition.kind === OBJECT_TYPE_DEFINITION || + definition.kind === INPUT_OBJECT_TYPE_DEFINITION || + definition.kind === ENUM_TYPE_DEFINITION + ) { + cache.set(definition.name.value, { + filePath, + content: query, + definition, + }); + } + }); + }); + } + } + + async updateObjectTypeDefinitionCache( + rootDir: Uri, + filePath: Uri, + exists: boolean, + ): Promise { + const fileAndContent = exists + ? await this.promiseToReadGraphQLFile(filePath) + : null; + // In the case of type definitions, the cache could just map the + // definition name to the parsed ast, whether or not it existed + // previously. + // For delete, remove the entry from the set. + if (!exists) { + const cache = this._typeDefinitionsCache.get(rootDir); + if (cache) { + cache.delete(filePath); + } + } else if (fileAndContent && fileAndContent.queries) { + this.updateObjectTypeDefinition( + rootDir, + filePath, + fileAndContent.queries, + ); + } + } + _extendSchema( schema: GraphQLSchema, schemaPath: ?string, @@ -544,12 +721,13 @@ export class GraphQLCache implements GraphQLCacheInterface { } /** - * Given a list of GraphQL file metadata, read all files collected from watchman - * and create fragmentDefinitions and GraphQL files cache. - */ + * Given a list of GraphQL file metadata, read all files collected from watchman + * and create fragmentDefinitions and GraphQL files cache. + */ readAllGraphQLFiles = async ( list: Array, ): Promise<{ + objectTypeDefinitions: Map, fragmentDefinitions: Map, graphQLFileMap: Map, }> => { @@ -561,13 +739,13 @@ export class GraphQLCache implements GraphQLCacheInterface { this.promiseToReadGraphQLFile(fileInfo.filePath) .catch(error => { /** - * fs emits `EMFILE | ENFILE` error when there are too many - * open files - this can cause some fragment files not to be - * processed. Solve this case by implementing a queue to save - * files failed to be processed because of `EMFILE` error, - * and await on Promises created with the next batch from the - * queue. - */ + * fs emits `EMFILE | ENFILE` error when there are too many + * open files - this can cause some fragment files not to be + * processed. Solve this case by implementing a queue to save + * files failed to be processed because of `EMFILE` error, + * and await on Promises created with the next batch from the + * queue. + */ if (error.code === 'EMFILE' || error.code === 'ENFILE') { queue.push(fileInfo); } @@ -587,15 +765,17 @@ export class GraphQLCache implements GraphQLCacheInterface { }; /** - * Takes an array of GraphQL File information and batch-processes into a - * map of fragmentDefinitions and GraphQL file cache. - */ + * Takes an array of GraphQL File information and batch-processes into a + * map of fragmentDefinitions and GraphQL file cache. + */ processGraphQLFiles = ( responses: Array, ): { + objectTypeDefinitions: Map, fragmentDefinitions: Map, graphQLFileMap: Map, } => { + const objectTypeDefinitions = new Map(); const fragmentDefinitions = new Map(); const graphQLFileMap = new Map(); @@ -612,6 +792,17 @@ export class GraphQLCache implements GraphQLCacheInterface { definition, }); } + if ( + definition.kind === OBJECT_TYPE_DEFINITION || + definition.kind === INPUT_OBJECT_TYPE_DEFINITION || + definition.kind === ENUM_TYPE_DEFINITION + ) { + objectTypeDefinitions.set(definition.name.value, { + filePath, + content, + definition, + }); + } }); }); } @@ -626,13 +817,13 @@ export class GraphQLCache implements GraphQLCacheInterface { }); }); - return {fragmentDefinitions, graphQLFileMap}; + return {objectTypeDefinitions, fragmentDefinitions, graphQLFileMap}; }; /** - * Returns a Promise to read a GraphQL file and return a GraphQL metadata - * including a parsed AST. - */ + * Returns a Promise to read a GraphQL file and return a GraphQL metadata + * including a parsed AST. + */ promiseToReadGraphQLFile = ( filePath: Uri, ): Promise<{ diff --git a/packages/server/src/Logger.js b/packages/server/src/Logger.js index 8f644ed7..6b9981d3 100644 --- a/packages/server/src/Logger.js +++ b/packages/server/src/Logger.js @@ -38,8 +38,9 @@ export class Logger implements VSCodeLogger { this._logFilePath = join( dir, - `graphql-language-service-log-${os.userInfo() - .username}-${getDateString()}.log`, + `graphql-language-service-log-${ + os.userInfo().username + }-${getDateString()}.log`, ); this._stream = null; diff --git a/packages/server/src/MessageProcessor.js b/packages/server/src/MessageProcessor.js index e155ee5a..7ad6aa7b 100644 --- a/packages/server/src/MessageProcessor.js +++ b/packages/server/src/MessageProcessor.js @@ -279,6 +279,7 @@ export class MessageProcessor { } this._updateFragmentDefinition(uri, contents); + this._updateObjectTypeDefinition(uri, contents); // Send the diagnostics onChange as well const diagnostics = []; @@ -482,6 +483,7 @@ export class MessageProcessor { const contents = getQueryAndRange(text, uri); this._updateFragmentDefinition(uri, contents); + this._updateObjectTypeDefinition(uri, contents); const diagnostics = (await Promise.all( contents.map(async ({query, range}) => { @@ -515,6 +517,11 @@ export class MessageProcessor { change.uri, false, ); + this._graphQLCache.updateObjectTypeDefinitionCache( + this._graphQLCache.getGraphQLConfig().configDir, + change.uri, + false, + ); } }), ); @@ -612,6 +619,19 @@ export class MessageProcessor { ); } + async _updateObjectTypeDefinition( + uri: Uri, + contents: Array, + ): Promise { + const rootDir = this._graphQLCache.getGraphQLConfig().configDir; + + await this._graphQLCache.updateObjectTypeDefinition( + rootDir, + new URL(uri).pathname, + contents, + ); + } + _getCachedDocument(uri: string): ?CachedDocumentType { if (this._textDocumentCache.has(uri)) { const cachedDocument = this._textDocumentCache.get(uri); diff --git a/packages/server/src/__tests__/GraphQLCache-test.js b/packages/server/src/__tests__/GraphQLCache-test.js index 51e6ad40..ca269fe9 100644 --- a/packages/server/src/__tests__/GraphQLCache-test.js +++ b/packages/server/src/__tests__/GraphQLCache-test.js @@ -183,4 +183,41 @@ describe('GraphQLCache', () => { expect(fragmentDefinitions.get('testFragment')).to.be.undefined; }); }); + + describe('getNamedTypeDependencies', () => { + const query = `type Query { + hero(episode: Episode): Character + } + + type Episode { + id: ID! + } + `; + const parsedQuery = parse(query); + + const namedTypeDefinitions = new Map(); + namedTypeDefinitions.set('Character', { + file: 'someOtherFilePath', + content: query, + definition: { + kind: 'ObjectTypeDefinition', + name: { + kind: 'Name', + value: 'Character', + }, + loc: { + start: 0, + end: 0, + }, + }, + }); + + it('finds named types referenced from the SDL', async () => { + const result = await cache.getObjectTypeDependenciesForAST( + parsedQuery, + namedTypeDefinitions, + ); + expect(result.length).to.equal(1); + }); + }); }); diff --git a/packages/server/src/__tests__/MessageProcessor-test.js b/packages/server/src/__tests__/MessageProcessor-test.js index 9bbe61f8..07f4bd00 100644 --- a/packages/server/src/__tests__/MessageProcessor-test.js +++ b/packages/server/src/__tests__/MessageProcessor-test.js @@ -48,6 +48,7 @@ describe('MessageProcessor', () => { }; }, updateFragmentDefinition() {}, + updateObjectTypeDefinition() {}, handleWatchmanSubscribeEvent() {}, }; messageProcessor._languageService = { diff --git a/packages/types/src/index.js b/packages/types/src/index.js index 2c29af75..d136cf9d 100644 --- a/packages/types/src/index.js +++ b/packages/types/src/index.js @@ -13,6 +13,8 @@ import type { ASTNode, DocumentNode, FragmentDefinitionNode, + NamedTypeNode, + TypeDefinitionNode, } from 'graphql/language'; import type {ValidationContext} from 'graphql/validation'; import type { @@ -30,26 +32,26 @@ import type {GraphQLConfig, GraphQLProjectConfig} from 'graphql-config'; export type TokenPattern = string | ((char: string) => boolean) | RegExp; export interface CharacterStream { - getStartOfToken: () => number, - getCurrentPosition: () => number, - eol: () => boolean, - sol: () => boolean, - peek: () => string | null, - next: () => string, - eat: (pattern: TokenPattern) => string | void, - eatWhile: (match: TokenPattern) => boolean, - eatSpace: () => boolean, - skipToEnd: () => void, - skipTo: (position: number) => void, + getStartOfToken: () => number; + getCurrentPosition: () => number; + eol: () => boolean; + sol: () => boolean; + peek: () => string | null; + next: () => string; + eat: (pattern: TokenPattern) => string | void; + eatWhile: (match: TokenPattern) => boolean; + eatSpace: () => boolean; + skipToEnd: () => void; + skipTo: (position: number) => void; match: ( pattern: TokenPattern, consume: ?boolean, caseFold: ?boolean, - ) => Array | boolean, - backUp: (num: number) => void, - column: () => number, - indentation: () => number, - current: () => string, + ) => Array | boolean; + backUp: (num: number) => void; + column: () => number; + indentation: () => number; + current: () => string; } // Cache and config-related. @@ -80,56 +82,82 @@ export type GraphQLConfigurationExtension = { }; export interface GraphQLCache { - getGraphQLConfig: () => GraphQLConfig, + getGraphQLConfig: () => GraphQLConfig; + + getObjectTypeDependencies: ( + query: string, + fragmentDefinitions: ?Map, + ) => Promise>; + + getObjectTypeDependenciesForAST: ( + parsedQuery: ASTNode, + fragmentDefinitions: Map, + ) => Promise>; + + getObjectTypeDefinitions: ( + graphQLConfig: GraphQLProjectConfig, + ) => Promise>; + + +updateObjectTypeDefinition: ( + rootDir: Uri, + filePath: Uri, + contents: Array, + ) => Promise; + + +updateObjectTypeDefinitionCache: ( + rootDir: Uri, + filePath: Uri, + exists: boolean, + ) => Promise; getFragmentDependencies: ( query: string, fragmentDefinitions: ?Map, - ) => Promise>, + ) => Promise>; getFragmentDependenciesForAST: ( parsedQuery: ASTNode, fragmentDefinitions: Map, - ) => Promise>, + ) => Promise>; getFragmentDefinitions: ( graphQLConfig: GraphQLProjectConfig, - ) => Promise>, + ) => Promise>; +updateFragmentDefinition: ( rootDir: Uri, filePath: Uri, contents: Array, - ) => Promise, + ) => Promise; +updateFragmentDefinitionCache: ( rootDir: Uri, filePath: Uri, exists: boolean, - ) => Promise, + ) => Promise; getSchema: ( appName: ?string, queryHasExtensions?: ?boolean, - ) => Promise, + ) => Promise; handleWatchmanSubscribeEvent: ( rootDir: string, projectConfig: GraphQLProjectConfig, - ) => (result: Object) => void, + ) => (result: Object) => void; } // online-parser related export interface Position { - line: number, - character: number, - lessThanOrEqualTo: (position: Position) => boolean, + line: number; + character: number; + lessThanOrEqualTo: (position: Position) => boolean; } export interface Range { - start: Position, - end: Position, - containsPosition: (position: Position) => boolean, + start: Position; + end: Position; + containsPosition: (position: Position) => boolean; } export type CachedContent = { @@ -212,6 +240,18 @@ export type FragmentInfo = { definition: FragmentDefinitionNode, }; +export type NamedTypeInfo = { + filePath?: Uri, + content: string, + definition: NamedTypeNode, +}; + +export type ObjectTypeInfo = { + filePath?: Uri, + content: string, + definition: TypeDefinitionNode, +}; + export type CustomValidationRule = (context: ValidationContext) => Object; export type Diagnostic = { @@ -280,11 +320,11 @@ export type Outline = { }; export interface DidChangeWatchedFilesParams { - changes: FileEvent[], + changes: FileEvent[]; } export interface FileEvent { - uri: string, - type: FileChangeType, + uri: string; + type: FileChangeType; } export const FileChangeTypeKind = { Created: 1, diff --git a/packages/utils/src/validateWithCustomRules.js b/packages/utils/src/validateWithCustomRules.js index 78587bf1..63aacea0 100644 --- a/packages/utils/src/validateWithCustomRules.js +++ b/packages/utils/src/validateWithCustomRules.js @@ -30,7 +30,10 @@ export function validateWithCustomRules( const { NoUnusedFragments, } = require('graphql/validation/rules/NoUnusedFragments'); - const rulesToSkip = [NoUnusedFragments]; + const { + ExecutableDefinitions, + } = require('graphql/validation/rules/ExecutableDefinitions'); + const rulesToSkip = [NoUnusedFragments, ExecutableDefinitions]; if (isRelayCompatMode) { const { KnownFragmentNames, diff --git a/resources/pretty.js b/resources/pretty.js index a70325f2..2e414bd9 100755 --- a/resources/pretty.js +++ b/resources/pretty.js @@ -33,7 +33,7 @@ const {stdout, stderr, status, error} = spawnSync(executable, [ glob, ]); const out = stdout.toString().trim(); -const err = stdout.toString().trim(); +const err = stderr.toString().trim(); function print(message) { if (message) { diff --git a/yarn.lock b/yarn.lock index 5d2f8084..57a89e23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2673,9 +2673,9 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@^1.5.3: - version "1.7.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.0.tgz#47481588f41f7c90f63938feb202ac82554e7150" +prettier@1.13: + version "1.13.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.3.tgz#e74c09a7df6519d472ca6febaa37cf7addb48a20" private@^0.1.6, private@^0.1.7: version "0.1.7"