jsx-no-bind.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /**
  2. * @fileoverview Prevents usage of Function.prototype.bind and arrow functions
  3. * in React component props.
  4. * @author Daniel Lo Nigro <dan.cx>
  5. * @author Jacky Ho
  6. */
  7. 'use strict';
  8. const propName = require('jsx-ast-utils/propName');
  9. const Components = require('../util/Components');
  10. const docsUrl = require('../util/docsUrl');
  11. // -----------------------------------------------------------------------------
  12. // Rule Definition
  13. // -----------------------------------------------------------------------------
  14. const violationMessageStore = {
  15. bindCall: 'JSX props should not use .bind()',
  16. arrowFunc: 'JSX props should not use arrow functions',
  17. bindExpression: 'JSX props should not use ::',
  18. func: 'JSX props should not use functions'
  19. };
  20. module.exports = {
  21. meta: {
  22. docs: {
  23. description: 'Prevents usage of Function.prototype.bind and arrow functions in React component props',
  24. category: 'Best Practices',
  25. recommended: false,
  26. url: docsUrl('jsx-no-bind')
  27. },
  28. schema: [{
  29. type: 'object',
  30. properties: {
  31. allowArrowFunctions: {
  32. default: false,
  33. type: 'boolean'
  34. },
  35. allowBind: {
  36. default: false,
  37. type: 'boolean'
  38. },
  39. allowFunctions: {
  40. default: false,
  41. type: 'boolean'
  42. },
  43. ignoreRefs: {
  44. default: false,
  45. type: 'boolean'
  46. }
  47. },
  48. additionalProperties: false
  49. }]
  50. },
  51. create: Components.detect(context => {
  52. const configuration = context.options[0] || {};
  53. // Keep track of all the variable names pointing to a bind call,
  54. // bind expression or an arrow function in different block statements
  55. const blockVariableNameSets = {};
  56. function setBlockVariableNameSet(blockStart) {
  57. blockVariableNameSets[blockStart] = {
  58. arrowFunc: new Set(),
  59. bindCall: new Set(),
  60. bindExpression: new Set(),
  61. func: new Set()
  62. };
  63. }
  64. function getNodeViolationType(node) {
  65. const nodeType = node.type;
  66. if (
  67. !configuration.allowBind &&
  68. nodeType === 'CallExpression' &&
  69. node.callee.type === 'MemberExpression' &&
  70. node.callee.property.type === 'Identifier' &&
  71. node.callee.property.name === 'bind'
  72. ) {
  73. return 'bindCall';
  74. } else if (
  75. nodeType === 'ConditionalExpression'
  76. ) {
  77. return getNodeViolationType(node.test) ||
  78. getNodeViolationType(node.consequent) ||
  79. getNodeViolationType(node.alternate);
  80. } else if (
  81. !configuration.allowArrowFunctions &&
  82. nodeType === 'ArrowFunctionExpression'
  83. ) {
  84. return 'arrowFunc';
  85. } else if (
  86. !configuration.allowFunctions &&
  87. nodeType === 'FunctionExpression'
  88. ) {
  89. return 'func';
  90. } else if (
  91. !configuration.allowBind &&
  92. nodeType === 'BindExpression'
  93. ) {
  94. return 'bindExpression';
  95. }
  96. return null;
  97. }
  98. function addVariableNameToSet(violationType, variableName, blockStart) {
  99. blockVariableNameSets[blockStart][violationType].add(variableName);
  100. }
  101. function getBlockStatementAncestors(node) {
  102. return context.getAncestors(node).reverse().filter(
  103. ancestor => ancestor.type === 'BlockStatement'
  104. );
  105. }
  106. function reportVariableViolation(node, name, blockStart) {
  107. const blockSets = blockVariableNameSets[blockStart];
  108. const violationTypes = Object.keys(blockSets);
  109. return violationTypes.find(type => {
  110. if (blockSets[type].has(name)) {
  111. context.report({node: node, message: violationMessageStore[type]});
  112. return true;
  113. }
  114. return false;
  115. });
  116. }
  117. function findVariableViolation(node, name) {
  118. getBlockStatementAncestors(node).find(
  119. block => reportVariableViolation(node, name, block.start)
  120. );
  121. }
  122. return {
  123. BlockStatement(node) {
  124. setBlockVariableNameSet(node.start);
  125. },
  126. VariableDeclarator(node) {
  127. if (!node.init) {
  128. return;
  129. }
  130. const blockAncestors = getBlockStatementAncestors(node);
  131. const variableViolationType = getNodeViolationType(node.init);
  132. if (
  133. blockAncestors.length > 0 &&
  134. variableViolationType &&
  135. node.parent.kind === 'const' // only support const right now
  136. ) {
  137. addVariableNameToSet(
  138. variableViolationType, node.id.name, blockAncestors[0].start
  139. );
  140. }
  141. },
  142. JSXAttribute: function (node) {
  143. const isRef = configuration.ignoreRefs && propName(node) === 'ref';
  144. if (isRef || !node.value || !node.value.expression) {
  145. return;
  146. }
  147. const valueNode = node.value.expression;
  148. const valueNodeType = valueNode.type;
  149. const nodeViolationType = getNodeViolationType(valueNode);
  150. if (valueNodeType === 'Identifier') {
  151. findVariableViolation(node, valueNode.name);
  152. } else if (nodeViolationType) {
  153. context.report({
  154. node: node, message: violationMessageStore[nodeViolationType]
  155. });
  156. }
  157. }
  158. };
  159. })
  160. };