diff --git a/src/rule-generated-flow-types.js b/src/rule-generated-flow-types.js index 5dd0883..dc6324d 100644 --- a/src/rule-generated-flow-types.js +++ b/src/rule-generated-flow-types.js @@ -195,6 +195,23 @@ function validateObjectTypeAnnotation( return true; } +/** + * Tries to find a GraphQL definition node for a given argument. + * Currently, only supports a graphql`...` literal inline, but could be + * improved to follow a variable definition. + */ +function getDefinitionName(arg) { + if (arg.type !== 'TaggedTemplateExpression') { + // TODO: maybe follow variables, see context.getScope() + return null; + } + const ast = getGraphQLAST(arg); + if (ast == null || ast.definitions.length === 0) { + return null; + } + return ast.definitions[0].name.value; +} + module.exports = { meta: { fixable: 'code', @@ -226,6 +243,7 @@ module.exports = { const imports = []; const requires = []; const typeAliasMap = {}; + const useFragmentInstances = []; return { ImportDeclaration(node) { imports.push(node); @@ -242,6 +260,44 @@ module.exports = { TypeAlias(node) { typeAliasMap[node.id.name] = node.right; }, + + /** + * Find useQuery() calls without type arguments. + */ + 'CallExpression[callee.name=useQuery]:not([typeArguments])'(node) { + const firstArg = node.arguments[0]; + if (firstArg == null) { + return; + } + const queryName = getDefinitionName(firstArg) || 'ExampleQuery'; + context.report({ + node: node, + message: + 'The `useQuery` hook should be used with an explicit generated Flow type, e.g.: useQuery<{{queryName}}>(...)', + data: { + queryName: queryName + } + }); + }, + + /** + * useFragment() calls + */ + 'CallExpression[callee.name=useFragment]'(node) { + const firstArg = node.arguments[0]; + if (firstArg == null) { + return; + } + const fragmentName = getDefinitionName(firstArg); + if (fragmentName == null) { + return; + } + useFragmentInstances.push({ + fragmentName: fragmentName, + node: node + }); + }, + ClassDeclaration(node) { const componentName = node.id.name; componentMap[componentName] = { @@ -282,6 +338,27 @@ module.exports = { }); }, 'Program:exit': function(_node) { + useFragmentInstances.forEach(useFragmentInstance => { + const fragmentName = useFragmentInstance.fragmentName; + const node = useFragmentInstance.node; + const foundImport = imports.find(importDeclaration => { + const importedFromModuleName = importDeclaration.source.value; + return importedFromModuleName.endsWith(fragmentName + '.graphql'); + }); + if (!foundImport) { + context.report({ + node: node, + message: + 'The prop passed to useFragment() should be typed with the ' + + 'type {{name}} imported from {{name}}.graphql, e.g.:\n' + + '\n' + + " import type {{{name}}} from '{{name}}.graphql;", + data: { + name: fragmentName + } + }); + } + }); expectedTypes.forEach(type => { const componentName = type.split('_')[0]; const propName = type diff --git a/test/test.js b/test/test.js index 516b0cf..8daa8e0 100644 --- a/test/test.js +++ b/test/test.js @@ -208,6 +208,19 @@ ruleTester.run('generated-flow-types', rules['generated-flow-types'], { valid: valid.concat([ // syntax error, covered by `graphql-syntax` {code: 'graphql`query {{{`'}, + { + code: ` + import type {TestFragment_foo} from 'TestFragment_foo.graphql'; + useFragment(graphql\`query TestFragment_foo { id }\`) + ` + }, + { + code: ` + import type {TestFragment_foo} from './path/to/TestFragment_foo.graphql'; + useFragment(graphql\`query TestFragment_foo { id }\`) + ` + }, + {code: 'useQuery(graphql`query Foo { id }`)'}, { code: ` import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' @@ -424,6 +437,46 @@ ruleTester.run('generated-flow-types', rules['generated-flow-types'], { } ]), invalid: [ + { + code: ` + import type {TestFragment_other} from './path/to/TestFragment_other.graphql'; + useFragment(graphql\`query TestFragment_foo { id }\`) + `, + errors: [ + { + message: ` +The prop passed to useFragment() should be typed with the type TestFragment_foo imported from TestFragment_foo.graphql, e.g.: + + import type {TestFragment_foo} from 'TestFragment_foo.graphql;`.trim(), + line: 3, + column: 9 + } + ] + }, + { + code: 'useQuery(graphql`query FooQuery { id }`)', + errors: [ + { + message: + 'The `useQuery` hook should be used with an explicit generated Flow type, e.g.: useQuery(...)', + line: 1, + column: 1 + } + ] + }, + { + code: ` + const query = graphql\`query FooQuery { id }\`; + useQuery(query); + `, + errors: [ + { + message: + 'The `useQuery` hook should be used with an explicit generated Flow type, e.g.: useQuery(...)', + line: 3 + } + ] + }, { filename: 'MyComponent.jsx', code: `