jsx-no-bind.js 6.0 KB

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