rule-compat-uses-vars.js 4.63 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
/**
 * 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
              });
            }
          }
        });
      }
    };
  }
};