in src/rule-generated-flow-types.js [305:965]
create(context) {
if (!shouldLint(context)) {
return {};
}
const options = getOptions(context.options[0]);
const componentMap = {};
const expectedTypes = [];
const imports = [];
const requires = [];
const typeAliasMap = {};
const useFragmentInstances = [];
/**
* Tries to find a GraphQL definition node for a given argument.
* Supports a graphql`...` literal inline and follows variable definitions.
*/
function getDefinition(arg) {
if (arg == null) {
return null;
}
if (arg.type === 'Identifier') {
const name = arg.name;
let scope = context.getScope();
while (scope && scope.type != 'global') {
for (const variable of scope.variables) {
if (variable.name === name) {
const definition = variable.defs.find(
def => def.node && def.node.type === 'VariableDeclarator'
);
return definition ? getDefinition(definition.node.init) : null;
}
}
scope = scope.upper;
}
return null;
}
if (arg.type !== 'TaggedTemplateExpression') {
return null;
}
return getGraphQLAST(arg);
}
function getDefinitionName(arg) {
const ast = getDefinition(arg);
if (ast == null || ast.definitions.length === 0) {
return null;
}
return ast.definitions[0].name.value;
}
function getRefetchableQueryName(arg) {
const ast = getDefinition(arg);
if (ast == null || ast.definitions.length === 0) {
return null;
}
const refetchable = ast.definitions[0].directives.find(
d => d.name.value === 'refetchable'
);
if (!refetchable) {
return null;
}
const nameArg = refetchable.arguments.find(
a => a.name.value === 'queryName'
);
return nameArg && nameArg.value && nameArg.value.value
? nameArg.value.value
: null;
}
function trackHookCall(node, hookName) {
const firstArg = node.arguments[0];
if (firstArg == null) {
return;
}
const fragmentName = getDefinitionName(firstArg);
if (fragmentName == null) {
return;
}
useFragmentInstances.push({
fragmentName: fragmentName,
node: node,
hookName: hookName
});
}
function createTypeImportFixer(node, operationName, typeText) {
return fixer => {
const importFixRange = genImportFixRange(
operationName,
imports,
requires
);
return [
genImportFixer(
fixer,
importFixRange,
operationName,
options.haste,
''
),
fixer.insertTextAfter(node.callee, `<${typeText}>`)
];
};
}
function reportAndFixRefetchableType(node, hookName, defaultQueryName) {
const queryName = getRefetchableQueryName(node.arguments[0]);
context.report({
node: node,
message: `The \`${hookName}\` hook should be used with an explicit generated Flow type, e.g.: ${hookName}<{{queryName}}, _>(...)`,
data: {
queryName: queryName || defaultQueryName
},
fix:
queryName != null && options.fix
? createTypeImportFixer(node, queryName, `${queryName}, _`)
: null
});
}
return {
ImportDeclaration(node) {
imports.push(node);
},
VariableDeclarator(node) {
if (
node.init &&
node.init.type === 'CallExpression' &&
node.init.callee.name === 'require'
) {
requires.push(node);
}
},
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);
context.report({
node: node,
message:
'The `useQuery` hook should be used with an explicit generated Flow type, e.g.: useQuery<{{queryName}}>(...)',
data: {
queryName: queryName || 'ExampleQuery'
},
fix:
queryName != null && options.fix
? createTypeImportFixer(node, queryName, queryName)
: null
});
},
/**
* Find useLazyLoadQuery() calls without type arguments.
*/
'CallExpression[callee.name=useLazyLoadQuery]:not([typeArguments])'(
node
) {
const firstArg = node.arguments[0];
if (firstArg == null) {
return;
}
const queryName = getDefinitionName(firstArg);
context.report({
node: node,
message:
'The `useLazyLoadQuery` hook should be used with an explicit generated Flow type, e.g.: useLazyLoadQuery<{{queryName}}>(...)',
data: {
queryName: queryName || 'ExampleQuery'
},
fix:
queryName != null && options.fix
? createTypeImportFixer(node, queryName, queryName)
: null
});
},
/**
* Find commitMutation() calls without type arguments.
*/
'CallExpression[callee.name=commitMutation]:not([typeArguments])'(node) {
// Get mutation config. It should be second argument of the `commitMutation`
const mutationConfig = node.arguments && node.arguments[1];
if (
mutationConfig == null ||
mutationConfig.type !== 'ObjectExpression'
) {
return;
}
// Find `mutation` property on the `mutationConfig`
const mutationNameProperty = mutationConfig.properties.find(
prop => prop.key != null && prop.key.name === 'mutation'
);
if (
mutationNameProperty == null ||
mutationNameProperty.value == null
) {
return;
}
const mutationName = getDefinitionName(mutationNameProperty.value);
context.report({
node: node,
message:
'The `commitMutation` must be used with an explicit generated Flow type, e.g.: commitMutation<{{mutationName}}>(...)',
data: {
mutationName: mutationName || 'ExampleMutation'
},
fix:
mutationName != null && options.fix
? createTypeImportFixer(node, mutationName, mutationName)
: null
});
},
/**
* Find requestSubscription() calls without type arguments.
*/
'CallExpression[callee.name=requestSubscription]:not([typeArguments])'(
node
) {
const subscriptionConfig = node.arguments && node.arguments[1];
if (
subscriptionConfig == null ||
subscriptionConfig.type !== 'ObjectExpression'
) {
return;
}
const subscriptionNameProperty = subscriptionConfig.properties.find(
prop => prop.key != null && prop.key.name === 'subscription'
);
if (
subscriptionNameProperty == null ||
subscriptionNameProperty.value == null
) {
return;
}
const subscriptionName = getDefinitionName(
subscriptionNameProperty.value
);
context.report({
node: node,
message:
'The `requestSubscription` must be used with an explicit generated Flow type, e.g.: requestSubscription<{{subscriptionName}}>(...)',
data: {
subscriptionName: subscriptionName || 'ExampleSubscription'
},
fix:
subscriptionName != null && options.fix
? createTypeImportFixer(node, subscriptionName, subscriptionName)
: null
});
},
/**
* Find useMutation() calls without type arguments.
*/
'CallExpression[callee.name=useMutation]:not([typeArguments])'(node) {
const queryName = getDefinitionName(node.arguments[0]);
context.report({
node,
message: `The \`useMutation\` hook should be used with an explicit generated Flow type, e.g.: useMutation<{{queryName}}>(...)`,
data: {
queryName: queryName
},
fix:
queryName != null && options.fix
? createTypeImportFixer(node, queryName, queryName)
: null
});
},
/**
* Find usePaginationFragment() calls without type arguments.
*/
'CallExpression[callee.name=usePaginationFragment]:not([typeArguments])'(
node
) {
reportAndFixRefetchableType(
node,
'usePaginationFragment',
'PaginationQuery'
);
},
/**
* Find useBlockingPaginationFragment() calls without type arguments.
*/
'CallExpression[callee.name=useBlockingPaginationFragment]:not([typeArguments])'(
node
) {
reportAndFixRefetchableType(
node,
'useBlockingPaginationFragment',
'PaginationQuery'
);
},
/**
* Find useLegacyPaginationFragment() calls without type arguments.
*/
'CallExpression[callee.name=useLegacyPaginationFragment]:not([typeArguments])'(
node
) {
reportAndFixRefetchableType(
node,
'useLegacyPaginationFragment',
'PaginationQuery'
);
},
/**
* Find useRefetchableFragment() calls without type arguments.
*/
'CallExpression[callee.name=useRefetchableFragment]:not([typeArguments])'(
node
) {
reportAndFixRefetchableType(
node,
'useRefetchableFragment',
'RefetchableQuery'
);
},
/**
* useFragment() calls
*/
'CallExpression[callee.name=useFragment]'(node) {
trackHookCall(node, 'useFragment');
},
/**
* usePaginationFragment() calls
*/
'CallExpression[callee.name=usePaginationFragment]'(node) {
trackHookCall(node, 'usePaginationFragment');
},
/**
* useBlockingPaginationFragment() calls
*/
'CallExpression[callee.name=useBlockingPaginationFragment]'(node) {
trackHookCall(node, 'useBlockingPaginationFragment');
},
/**
* useLegacyPaginationFragment() calls
*/
'CallExpression[callee.name=useLegacyPaginationFragment]'(node) {
trackHookCall(node, 'useLegacyPaginationFragment');
},
/**
* useRefetchableFragment() calls
*/
'CallExpression[callee.name=useRefetchableFragment]'(node) {
trackHookCall(node, 'useRefetchableFragment');
},
ClassDeclaration(node) {
const componentName = node.id.name;
componentMap[componentName] = {
Component: node.id
};
// new style React.Component accepts 'props' as the first parameter
if (node.superTypeParameters && node.superTypeParameters.params[0]) {
componentMap[componentName].propType =
node.superTypeParameters.params[0];
}
},
TaggedTemplateExpression(node) {
const ast = getGraphQLAST(node);
if (!ast) {
return;
}
ast.definitions.forEach(def => {
if (!def.name) {
// no name, covered by graphql-naming/TaggedTemplateExpression
return;
}
if (def.kind === 'FragmentDefinition') {
expectedTypes.push(def.name.value);
}
});
},
'Program:exit': function (_node) {
useFragmentInstances.forEach(useFragmentInstance => {
const fragmentName = useFragmentInstance.fragmentName;
const hookName = useFragmentInstance.hookName;
const node = useFragmentInstance.node;
const foundImport = imports.some(importDeclaration => {
const importedFromModuleName = importDeclaration.source.value;
// `includes()` to allow a suffix like `.js` or path prefixes
if (!importedFromModuleName.includes(fragmentName + '.graphql')) {
return false;
}
// import type {...} from '...';
if (importDeclaration.importKind === 'type') {
return importDeclaration.specifiers.some(
specifier =>
specifier.type === 'ImportSpecifier' &&
specifier.imported.name === fragmentName + '$key'
);
}
// import {type xyz} from '...';
if (importDeclaration.importKind === 'value') {
return importDeclaration.specifiers.some(
specifier =>
specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type' &&
specifier.imported.name === fragmentName + '$key'
);
}
return false;
});
if (foundImport) {
return;
}
// Check if the fragment ref that we're passing to the hook
// comes from a previous useFragment (or variants) hook call.
const fragmentRefArgName =
node.arguments[1] != null ? node.arguments[1].name : null;
const foundFragmentRefDeclaration = useFragmentInstances.some(
_useFragmentInstance => {
if (_useFragmentInstance === useFragmentInstance) {
return false;
}
const variableDeclaratorNode = _useFragmentInstance.node.parent;
if (
!variableDeclaratorNode ||
!variableDeclaratorNode.id ||
!variableDeclaratorNode.id.type
) {
return false;
}
if (variableDeclaratorNode.id.type === 'Identifier') {
return (
fragmentRefArgName != null &&
variableDeclaratorNode.id.name === fragmentRefArgName
);
}
if (
variableDeclaratorNode.id.type === 'ObjectPattern' &&
variableDeclaratorNode.id.properties != null
) {
return variableDeclaratorNode.id.properties.some(prop => {
return (
fragmentRefArgName != null &&
prop &&
prop.value &&
prop.value.name === fragmentRefArgName
);
});
}
return false;
}
);
if (foundFragmentRefDeclaration) {
return;
}
context.report({
node: node,
message:
'The prop passed to {{hookName}}() should be typed with the ' +
"type '{{name}}$key' imported from '{{name}}.graphql', " +
'e.g.:\n' +
'\n' +
" import type {{{name}}$key} from '{{name}}.graphql';",
data: {
name: fragmentName,
hookName: hookName
}
});
});
expectedTypes.forEach(type => {
const componentName = type.split('_')[0];
const propName = type.split('_').slice(1).join('_');
if (!componentName || !propName || !componentMap[componentName]) {
// incorrect name, covered by graphql-naming/CallExpression
return;
}
const Component = componentMap[componentName].Component;
const propType = componentMap[componentName].propType;
// resolve local type alias
const importedPropType = imports.reduce((acc, node) => {
if (node.specifiers) {
const typeSpecifier = node.specifiers.find(specifier => {
if (specifier.type !== 'ImportSpecifier') {
return false;
}
return specifier.imported.name === type;
});
if (typeSpecifier) {
return typeSpecifier.local.name;
}
}
return acc;
}, type);
const importFixRange = genImportFixRange(
importedPropType,
imports,
requires
);
if (propType) {
// There exists a prop typeAnnotation. Let's look at how it's
// structured
switch (propType.type) {
case 'ObjectTypeAnnotation': {
validateObjectTypeAnnotation(
context,
Component,
importedPropType,
propName,
propType,
importFixRange,
typeAliasMap
);
break;
}
case 'GenericTypeAnnotation': {
const aliasedObjectType = extractReadOnlyType(
resolveTypeAlias(propType, typeAliasMap)
);
if (!aliasedObjectType) {
// The type Alias doesn't exist, is invalid, or is being
// imported. Can't do anything.
break;
}
switch (aliasedObjectType.type) {
case 'ObjectTypeAnnotation': {
validateObjectTypeAnnotation(
context,
Component,
importedPropType,
propName,
aliasedObjectType,
importFixRange,
typeAliasMap
);
break;
}
case 'IntersectionTypeAnnotation': {
const objectTypes = aliasedObjectType.types
.map(intersectedType => {
if (intersectedType.type === 'GenericTypeAnnotation') {
return extractReadOnlyType(
resolveTypeAlias(intersectedType, typeAliasMap)
);
}
if (intersectedType.type === 'ObjectTypeAnnotation') {
return intersectedType;
}
})
.filter(maybeObjectType => {
// GenericTypeAnnotation may not map to an object type
return (
maybeObjectType &&
maybeObjectType.type === 'ObjectTypeAnnotation'
);
});
if (!objectTypes.length) {
// The type Alias is likely being imported.
// Can't do anything.
break;
}
for (const objectType of objectTypes) {
const isValid = validateObjectTypeAnnotation(
context,
Component,
importedPropType,
propName,
objectType,
importFixRange,
typeAliasMap,
true // Return false if invalid instead of reporting
);
if (isValid) {
break;
}
}
// otherwise report an error at the first object
validateObjectTypeAnnotation(
context,
Component,
importedPropType,
propName,
objectTypes[0],
importFixRange,
typeAliasMap
);
break;
}
}
break;
}
}
} else {
context.report({
message:
'Component property `{{prop}}` expects to use the ' +
'generated `{{type}}` flow type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions',
data: {
prop: propName,
type: importedPropType
},
fix: options.fix
? fixer => {
const classBodyStart = Component.parent.body.body[0];
if (!classBodyStart) {
// HACK: There's nothing in the body. Let's not do anything
// When something is added to the body, we'll have a fix
return;
}
const aliasWhitespace = ' '.repeat(
Component.parent.loc.start.column
);
const propsWhitespace = ' '.repeat(
classBodyStart.loc.start.column
);
return [
genImportFixer(
fixer,
importFixRange,
importedPropType,
options.haste,
aliasWhitespace
),
fixer.insertTextBefore(
Component.parent,
`type Props = {${propName}: ` +
`${importedPropType}};\n\n${aliasWhitespace}`
),
fixer.insertTextBefore(
classBodyStart,
`props: Props;\n\n${propsWhitespace}`
)
];
}
: null,
loc: Component.loc
});
}
});
}
};
}