in src/rule-generated-flow-types.js [698:963]
'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
});
}
});
}