no-array-index-key.js 5.7 KB

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