blob: 21f6a297a9f778ef212d52fc1fdcb731002e13e9 [file] [log] [blame]
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const utils = require('./utils');
const shouldLint = utils.shouldLint;
const getGraphQLAST = utils.getGraphQLAST;
const getModuleName = utils.getModuleName;
const getLoc = utils.getLoc;
const graphql = require('graphql');
const visit = graphql.visit;
function validateInlineDirective(spreadNode) {
return !!spreadNode.directives
.filter(directive => directive.name.value === 'relay')
.find(
directive =>
!!directive.arguments.find(argument => argument.name.value === 'mask')
);
}
module.exports = {
meta: {
docs: {
description:
'Relay Compat transforms fragment spreads from ' +
"`...Container_foo` to `Container.getFragment('foo')`. This " +
'makes ESLint aware of this.'
}
},
create(context) {
if (!shouldLint(context)) {
return {};
}
if (!/react-relay\/compat|RelayCompat/.test(context.getSourceCode().text)) {
// Only run in for compat mode files
return {};
}
function isInScope(name) {
var scope = context.getScope();
var variables = scope.variables;
while (scope.type !== 'global') {
scope = scope.upper;
variables = scope.variables.concat(variables);
}
if (scope.childScopes.length) {
variables = scope.childScopes[0].variables.concat(variables);
// Temporary fix for babel-eslint
if (scope.childScopes[0].childScopes.length) {
variables = scope.childScopes[0].childScopes[0].variables.concat(
variables
);
}
}
for (var i = 0, len = variables.length; i < len; i++) {
if (variables[i].name === name) {
return true;
}
}
return false;
}
return {
TaggedTemplateExpression(taggedTemplateExpression) {
const ast = getGraphQLAST(taggedTemplateExpression);
if (!ast) {
return;
}
visit(ast, {
FragmentSpread(spreadNode) {
const m =
spreadNode.name &&
spreadNode.name.value.match(/^([a-z0-9]+)_([a-z0-9_]+)/i);
if (!m) {
return;
}
const componentName = m[1];
const propName = m[2];
const loc = getLoc(
context,
taggedTemplateExpression,
spreadNode.name
);
if (isInScope(componentName)) {
// if this variable is defined, mark it as used
context.markVariableAsUsed(componentName);
} else if (componentName === getModuleName(context.getFilename())) {
if (!validateInlineDirective(spreadNode)) {
context.report({
message:
'It looks like you are trying to spread the locally defined fragment `{{fragmentName}}`. ' +
'In compat mode, Relay only supports that for `@relay(mask: false)` directive. ' +
'If you intend to do that, please add the directive to the fragment spread `{{fragmentName}}` ' +
'and make sure that it is bound to a local variable named `{{propName}}`.',
data: {
fragmentName: spreadNode.name.value,
propName: propName
},
loc: loc
});
return;
}
if (!isInScope(propName)) {
context.report({
message:
'When you are unmasking the locally defined fragment spread `{{fragmentName}}`, please make sure ' +
'the fragment is bound to a variable named `{{propName}}`.',
data: {
fragmentName: spreadNode.name.value,
propName: propName
},
loc: loc
});
}
context.markVariableAsUsed(propName);
} else {
// otherwise, yell about this needed to be defined
context.report({
message:
'In compat mode, Relay expects the component that has ' +
'the `{{fragmentName}}` fragment to be imported with ' +
'the variable name `{{varName}}`.',
data: {
fragmentName: spreadNode.name.value,
varName: componentName
},
loc: loc
});
}
}
});
}
};
}
};