/** * 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 }); } } }); } }; } };