jsx-no-bind.js 5.3 KB

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