no-array-index-key.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /**
  2. * @fileoverview Prevent usage of Array index in keys
  3. * @author Joe Lencioni
  4. */
  5. 'use strict';
  6. const has = require('hasown');
  7. const astUtil = require('../util/ast');
  8. const docsUrl = require('../util/docsUrl');
  9. const pragma = require('../util/pragma');
  10. const report = require('../util/report');
  11. const variableUtil = require('../util/variable');
  12. // ------------------------------------------------------------------------------
  13. // Rule Definition
  14. // ------------------------------------------------------------------------------
  15. function isCreateCloneElement(node, context) {
  16. if (!node) {
  17. return false;
  18. }
  19. if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') {
  20. return node.object
  21. && node.object.name === pragma.getFromContext(context)
  22. && ['createElement', 'cloneElement'].indexOf(node.property.name) !== -1;
  23. }
  24. if (node.type === 'Identifier') {
  25. const variable = variableUtil.findVariableByName(context, node, node.name);
  26. if (variable && variable.type === 'ImportSpecifier') {
  27. return variable.parent.source.value === 'react';
  28. }
  29. }
  30. return false;
  31. }
  32. const messages = {
  33. noArrayIndex: 'Do not use Array index in keys',
  34. };
  35. /** @type {import('eslint').Rule.RuleModule} */
  36. module.exports = {
  37. meta: {
  38. docs: {
  39. description: 'Disallow usage of Array index in keys',
  40. category: 'Best Practices',
  41. recommended: false,
  42. url: docsUrl('no-array-index-key'),
  43. },
  44. messages,
  45. schema: [],
  46. },
  47. create(context) {
  48. // --------------------------------------------------------------------------
  49. // Public
  50. // --------------------------------------------------------------------------
  51. const indexParamNames = [];
  52. const iteratorFunctionsToIndexParamPosition = {
  53. every: 1,
  54. filter: 1,
  55. find: 1,
  56. findIndex: 1,
  57. flatMap: 1,
  58. forEach: 1,
  59. map: 1,
  60. reduce: 2,
  61. reduceRight: 2,
  62. some: 1,
  63. };
  64. function isArrayIndex(node) {
  65. return node.type === 'Identifier'
  66. && indexParamNames.indexOf(node.name) !== -1;
  67. }
  68. function isUsingReactChildren(node) {
  69. const callee = node.callee;
  70. if (
  71. !callee
  72. || !callee.property
  73. || !callee.object
  74. ) {
  75. return null;
  76. }
  77. const isReactChildMethod = ['map', 'forEach'].indexOf(callee.property.name) > -1;
  78. if (!isReactChildMethod) {
  79. return null;
  80. }
  81. const obj = callee.object;
  82. if (obj && obj.name === 'Children') {
  83. return true;
  84. }
  85. if (obj && obj.object && obj.object.name === pragma.getFromContext(context)) {
  86. return true;
  87. }
  88. return false;
  89. }
  90. function getMapIndexParamName(node) {
  91. const callee = node.callee;
  92. if (callee.type !== 'MemberExpression' && callee.type !== 'OptionalMemberExpression') {
  93. return null;
  94. }
  95. if (callee.property.type !== 'Identifier') {
  96. return null;
  97. }
  98. if (!has(iteratorFunctionsToIndexParamPosition, callee.property.name)) {
  99. return null;
  100. }
  101. const name = /** @type {keyof iteratorFunctionsToIndexParamPosition} */ (callee.property.name);
  102. const callbackArg = isUsingReactChildren(node)
  103. ? node.arguments[1]
  104. : node.arguments[0];
  105. if (!callbackArg) {
  106. return null;
  107. }
  108. if (!astUtil.isFunctionLikeExpression(callbackArg)) {
  109. return null;
  110. }
  111. const params = callbackArg.params;
  112. const indexParamPosition = iteratorFunctionsToIndexParamPosition[name];
  113. if (params.length < indexParamPosition + 1) {
  114. return null;
  115. }
  116. return params[indexParamPosition].name;
  117. }
  118. function getIdentifiersFromBinaryExpression(side) {
  119. if (side.type === 'Identifier') {
  120. return side;
  121. }
  122. if (side.type === 'BinaryExpression') {
  123. // recurse
  124. const left = getIdentifiersFromBinaryExpression(side.left);
  125. const right = getIdentifiersFromBinaryExpression(side.right);
  126. return [].concat(left, right).filter(Boolean);
  127. }
  128. return null;
  129. }
  130. function checkPropValue(node) {
  131. if (isArrayIndex(node)) {
  132. // key={bar}
  133. report(context, messages.noArrayIndex, 'noArrayIndex', {
  134. node,
  135. });
  136. return;
  137. }
  138. if (node.type === 'TemplateLiteral') {
  139. // key={`foo-${bar}`}
  140. node.expressions.filter(isArrayIndex).forEach(() => {
  141. report(context, messages.noArrayIndex, 'noArrayIndex', {
  142. node,
  143. });
  144. });
  145. return;
  146. }
  147. if (node.type === 'BinaryExpression') {
  148. // key={'foo' + bar}
  149. const identifiers = getIdentifiersFromBinaryExpression(node);
  150. identifiers.filter(isArrayIndex).forEach(() => {
  151. report(context, messages.noArrayIndex, 'noArrayIndex', {
  152. node,
  153. });
  154. });
  155. return;
  156. }
  157. if (
  158. astUtil.isCallExpression(node)
  159. && node.callee
  160. && node.callee.type === 'MemberExpression'
  161. && node.callee.object
  162. && isArrayIndex(node.callee.object)
  163. && node.callee.property
  164. && node.callee.property.type === 'Identifier'
  165. && node.callee.property.name === 'toString'
  166. ) {
  167. // key={bar.toString()}
  168. report(context, messages.noArrayIndex, 'noArrayIndex', {
  169. node,
  170. });
  171. return;
  172. }
  173. if (
  174. astUtil.isCallExpression(node)
  175. && node.callee
  176. && node.callee.type === 'Identifier'
  177. && node.callee.name === 'String'
  178. && Array.isArray(node.arguments)
  179. && node.arguments.length > 0
  180. && isArrayIndex(node.arguments[0])
  181. ) {
  182. // key={String(bar)}
  183. report(context, messages.noArrayIndex, 'noArrayIndex', {
  184. node: node.arguments[0],
  185. });
  186. }
  187. }
  188. function popIndex(node) {
  189. const mapIndexParamName = getMapIndexParamName(node);
  190. if (!mapIndexParamName) {
  191. return;
  192. }
  193. indexParamNames.pop();
  194. }
  195. return {
  196. 'CallExpression, OptionalCallExpression'(node) {
  197. if (isCreateCloneElement(node.callee, context) && node.arguments.length > 1) {
  198. // React.createElement
  199. if (!indexParamNames.length) {
  200. return;
  201. }
  202. const props = node.arguments[1];
  203. if (props.type !== 'ObjectExpression') {
  204. return;
  205. }
  206. props.properties.forEach((prop) => {
  207. if (!prop.key || prop.key.name !== 'key') {
  208. // { ...foo }
  209. // { foo: bar }
  210. return;
  211. }
  212. checkPropValue(prop.value);
  213. });
  214. return;
  215. }
  216. const mapIndexParamName = getMapIndexParamName(node);
  217. if (!mapIndexParamName) {
  218. return;
  219. }
  220. indexParamNames.push(mapIndexParamName);
  221. },
  222. JSXAttribute(node) {
  223. if (node.name.name !== 'key') {
  224. // foo={bar}
  225. return;
  226. }
  227. if (!indexParamNames.length) {
  228. // Not inside a call expression that we think has an index param.
  229. return;
  230. }
  231. const value = node.value;
  232. if (!value || value.type !== 'JSXExpressionContainer') {
  233. // key='foo' or just simply 'key'
  234. return;
  235. }
  236. checkPropValue(value.expression);
  237. },
  238. 'CallExpression:exit': popIndex,
  239. 'OptionalCallExpression:exit': popIndex,
  240. };
  241. },
  242. };