no-array-index-key.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. /**
  2. * @fileoverview Prevent usage of Array index in keys
  3. * @author Joe Lencioni
  4. */
  5. 'use strict';
  6. const has = require('has');
  7. const docsUrl = require('../util/docsUrl');
  8. // ------------------------------------------------------------------------------
  9. // Rule Definition
  10. // ------------------------------------------------------------------------------
  11. module.exports = {
  12. meta: {
  13. docs: {
  14. description: 'Prevent usage of Array index in keys',
  15. category: 'Best Practices',
  16. recommended: false,
  17. url: docsUrl('no-array-index-key')
  18. },
  19. schema: []
  20. },
  21. create: function(context) {
  22. // --------------------------------------------------------------------------
  23. // Public
  24. // --------------------------------------------------------------------------
  25. const indexParamNames = [];
  26. const iteratorFunctionsToIndexParamPosition = {
  27. every: 1,
  28. filter: 1,
  29. find: 1,
  30. findIndex: 1,
  31. forEach: 1,
  32. map: 1,
  33. reduce: 2,
  34. reduceRight: 2,
  35. some: 1
  36. };
  37. const ERROR_MESSAGE = 'Do not use Array index in keys';
  38. function isArrayIndex(node) {
  39. return node.type === 'Identifier'
  40. && indexParamNames.indexOf(node.name) !== -1;
  41. }
  42. function getMapIndexParamName(node) {
  43. const callee = node.callee;
  44. if (callee.type !== 'MemberExpression') {
  45. return null;
  46. }
  47. if (callee.property.type !== 'Identifier') {
  48. return null;
  49. }
  50. if (!has(iteratorFunctionsToIndexParamPosition, callee.property.name)) {
  51. return null;
  52. }
  53. const firstArg = node.arguments[0];
  54. if (!firstArg) {
  55. return null;
  56. }
  57. const isFunction = [
  58. 'ArrowFunctionExpression',
  59. 'FunctionExpression'
  60. ].indexOf(firstArg.type) !== -1;
  61. if (!isFunction) {
  62. return null;
  63. }
  64. const params = firstArg.params;
  65. const indexParamPosition = iteratorFunctionsToIndexParamPosition[callee.property.name];
  66. if (params.length < indexParamPosition + 1) {
  67. return null;
  68. }
  69. return params[indexParamPosition].name;
  70. }
  71. function getIdentifiersFromBinaryExpression(side) {
  72. if (side.type === 'Identifier') {
  73. return side;
  74. }
  75. if (side.type === 'BinaryExpression') {
  76. // recurse
  77. const left = getIdentifiersFromBinaryExpression(side.left);
  78. const right = getIdentifiersFromBinaryExpression(side.right);
  79. return [].concat(left, right).filter(Boolean);
  80. }
  81. return null;
  82. }
  83. function checkPropValue(node) {
  84. if (isArrayIndex(node)) {
  85. // key={bar}
  86. context.report({
  87. node: node,
  88. message: ERROR_MESSAGE
  89. });
  90. return;
  91. }
  92. if (node.type === 'TemplateLiteral') {
  93. // key={`foo-${bar}`}
  94. node.expressions.filter(isArrayIndex).forEach(() => {
  95. context.report({node: node, message: ERROR_MESSAGE});
  96. });
  97. return;
  98. }
  99. if (node.type === 'BinaryExpression') {
  100. // key={'foo' + bar}
  101. const identifiers = getIdentifiersFromBinaryExpression(node);
  102. identifiers.filter(isArrayIndex).forEach(() => {
  103. context.report({node: node, message: ERROR_MESSAGE});
  104. });
  105. return;
  106. }
  107. }
  108. return {
  109. CallExpression: function(node) {
  110. if (
  111. node.callee
  112. && node.callee.type === 'MemberExpression'
  113. && ['createElement', 'cloneElement'].indexOf(node.callee.property.name) !== -1
  114. && node.arguments.length > 1
  115. ) {
  116. // React.createElement
  117. if (!indexParamNames.length) {
  118. return;
  119. }
  120. const props = node.arguments[1];
  121. if (props.type !== 'ObjectExpression') {
  122. return;
  123. }
  124. props.properties.forEach(prop => {
  125. if (!prop.key || prop.key.name !== 'key') {
  126. // { ...foo }
  127. // { foo: bar }
  128. return;
  129. }
  130. checkPropValue(prop.value);
  131. });
  132. return;
  133. }
  134. const mapIndexParamName = getMapIndexParamName(node);
  135. if (!mapIndexParamName) {
  136. return;
  137. }
  138. indexParamNames.push(mapIndexParamName);
  139. },
  140. JSXAttribute: function(node) {
  141. if (node.name.name !== 'key') {
  142. // foo={bar}
  143. return;
  144. }
  145. if (!indexParamNames.length) {
  146. // Not inside a call expression that we think has an index param.
  147. return;
  148. }
  149. const value = node.value;
  150. if (!value || value.type !== 'JSXExpressionContainer') {
  151. // key='foo' or just simply 'key'
  152. return;
  153. }
  154. checkPropValue(value.expression);
  155. },
  156. 'CallExpression:exit': function(node) {
  157. const mapIndexParamName = getMapIndexParamName(node);
  158. if (!mapIndexParamName) {
  159. return;
  160. }
  161. indexParamNames.pop();
  162. }
  163. };
  164. }
  165. };