jsx-key.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /**
  2. * @fileoverview Report missing `key` props in iterators/collection literals.
  3. * @author Ben Mosher
  4. */
  5. 'use strict';
  6. const hasProp = require('jsx-ast-utils/hasProp');
  7. const propName = require('jsx-ast-utils/propName');
  8. const values = require('object.values');
  9. const docsUrl = require('../util/docsUrl');
  10. const pragmaUtil = require('../util/pragma');
  11. const report = require('../util/report');
  12. const astUtil = require('../util/ast');
  13. // ------------------------------------------------------------------------------
  14. // Rule Definition
  15. // ------------------------------------------------------------------------------
  16. const defaultOptions = {
  17. checkFragmentShorthand: false,
  18. checkKeyMustBeforeSpread: false,
  19. warnOnDuplicates: false,
  20. };
  21. const messages = {
  22. missingIterKey: 'Missing "key" prop for element in iterator',
  23. missingIterKeyUsePrag: 'Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
  24. missingArrayKey: 'Missing "key" prop for element in array',
  25. missingArrayKeyUsePrag: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
  26. keyBeforeSpread: '`key` prop must be placed before any `{...spread}, to avoid conflicting with React’s new JSX transform: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html`',
  27. nonUniqueKeys: '`key` prop must be unique',
  28. };
  29. module.exports = {
  30. meta: {
  31. docs: {
  32. description: 'Disallow missing `key` props in iterators/collection literals',
  33. category: 'Possible Errors',
  34. recommended: true,
  35. url: docsUrl('jsx-key'),
  36. },
  37. messages,
  38. schema: [{
  39. type: 'object',
  40. properties: {
  41. checkFragmentShorthand: {
  42. type: 'boolean',
  43. default: defaultOptions.checkFragmentShorthand,
  44. },
  45. checkKeyMustBeforeSpread: {
  46. type: 'boolean',
  47. default: defaultOptions.checkKeyMustBeforeSpread,
  48. },
  49. warnOnDuplicates: {
  50. type: 'boolean',
  51. default: defaultOptions.warnOnDuplicates,
  52. },
  53. },
  54. additionalProperties: false,
  55. }],
  56. },
  57. create(context) {
  58. const options = Object.assign({}, defaultOptions, context.options[0]);
  59. const checkFragmentShorthand = options.checkFragmentShorthand;
  60. const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread;
  61. const warnOnDuplicates = options.warnOnDuplicates;
  62. const reactPragma = pragmaUtil.getFromContext(context);
  63. const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
  64. function checkIteratorElement(node) {
  65. if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) {
  66. report(context, messages.missingIterKey, 'missingIterKey', {
  67. node,
  68. });
  69. } else if (checkFragmentShorthand && node.type === 'JSXFragment') {
  70. report(context, messages.missingIterKeyUsePrag, 'missingIterKeyUsePrag', {
  71. node,
  72. data: {
  73. reactPrag: reactPragma,
  74. fragPrag: fragmentPragma,
  75. },
  76. });
  77. }
  78. }
  79. function getReturnStatements(node) {
  80. const returnStatements = arguments[1] || [];
  81. if (node.type === 'IfStatement') {
  82. if (node.consequent) {
  83. getReturnStatements(node.consequent, returnStatements);
  84. }
  85. if (node.alternate) {
  86. getReturnStatements(node.alternate, returnStatements);
  87. }
  88. } else if (Array.isArray(node.body)) {
  89. node.body.forEach((item) => {
  90. if (item.type === 'IfStatement') {
  91. getReturnStatements(item, returnStatements);
  92. }
  93. if (item.type === 'ReturnStatement') {
  94. returnStatements.push(item);
  95. }
  96. });
  97. }
  98. return returnStatements;
  99. }
  100. function isKeyAfterSpread(attributes) {
  101. let hasFoundSpread = false;
  102. return attributes.some((attribute) => {
  103. if (attribute.type === 'JSXSpreadAttribute') {
  104. hasFoundSpread = true;
  105. return false;
  106. }
  107. if (attribute.type !== 'JSXAttribute') {
  108. return false;
  109. }
  110. return hasFoundSpread && propName(attribute) === 'key';
  111. });
  112. }
  113. /**
  114. * Checks if the given node is a function expression or arrow function,
  115. * and checks if there is a missing key prop in return statement's arguments
  116. * @param {ASTNode} node
  117. */
  118. function checkFunctionsBlockStatement(node) {
  119. if (astUtil.isFunctionLikeExpression(node)) {
  120. if (node.body.type === 'BlockStatement') {
  121. getReturnStatements(node.body)
  122. .filter((returnStatement) => returnStatement && returnStatement.argument)
  123. .forEach((returnStatement) => {
  124. checkIteratorElement(returnStatement.argument);
  125. });
  126. }
  127. }
  128. }
  129. /**
  130. * Checks if the given node is an arrow function that has an JSX Element or JSX Fragment in its body,
  131. * and the JSX is missing a key prop
  132. * @param {ASTNode} node
  133. */
  134. function checkArrowFunctionWithJSX(node) {
  135. const isArrFn = node && node.type === 'ArrowFunctionExpression';
  136. const shouldCheckNode = (n) => n && (n.type === 'JSXElement' || n.type === 'JSXFragment');
  137. if (isArrFn && shouldCheckNode(node.body)) {
  138. checkIteratorElement(node.body);
  139. }
  140. if (node.body.type === 'ConditionalExpression') {
  141. if (shouldCheckNode(node.body.consequent)) {
  142. checkIteratorElement(node.body.consequent);
  143. }
  144. if (shouldCheckNode(node.body.alternate)) {
  145. checkIteratorElement(node.body.alternate);
  146. }
  147. } else if (node.body.type === 'LogicalExpression' && shouldCheckNode(node.body.right)) {
  148. checkIteratorElement(node.body.right);
  149. }
  150. }
  151. const childrenToArraySelector = `:matches(
  152. CallExpression
  153. [callee.object.object.name=${reactPragma}]
  154. [callee.object.property.name=Children]
  155. [callee.property.name=toArray],
  156. CallExpression
  157. [callee.object.name=Children]
  158. [callee.property.name=toArray]
  159. )`.replace(/\s/g, '');
  160. let isWithinChildrenToArray = false;
  161. const seen = new WeakSet();
  162. return {
  163. [childrenToArraySelector]() {
  164. isWithinChildrenToArray = true;
  165. },
  166. [`${childrenToArraySelector}:exit`]() {
  167. isWithinChildrenToArray = false;
  168. },
  169. 'ArrayExpression, JSXElement > JSXElement'(node) {
  170. if (isWithinChildrenToArray) {
  171. return;
  172. }
  173. const jsx = (node.type === 'ArrayExpression' ? node.elements : node.parent.children).filter((x) => x && x.type === 'JSXElement');
  174. if (jsx.length === 0) {
  175. return;
  176. }
  177. const map = {};
  178. jsx.forEach((element) => {
  179. const attrs = element.openingElement.attributes;
  180. const keys = attrs.filter((x) => x.name && x.name.name === 'key');
  181. if (keys.length === 0) {
  182. if (node.type === 'ArrayExpression') {
  183. report(context, messages.missingArrayKey, 'missingArrayKey', {
  184. node: element,
  185. });
  186. }
  187. } else {
  188. keys.forEach((attr) => {
  189. const value = context.getSourceCode().getText(attr.value);
  190. if (!map[value]) { map[value] = []; }
  191. map[value].push(attr);
  192. if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) {
  193. report(context, messages.keyBeforeSpread, 'keyBeforeSpread', {
  194. node: node.type === 'ArrayExpression' ? node : node.parent,
  195. });
  196. }
  197. });
  198. }
  199. });
  200. if (warnOnDuplicates) {
  201. values(map).filter((v) => v.length > 1).forEach((v) => {
  202. v.forEach((n) => {
  203. if (!seen.has(n)) {
  204. seen.add(n);
  205. report(context, messages.nonUniqueKeys, 'nonUniqueKeys', {
  206. node: n,
  207. });
  208. }
  209. });
  210. });
  211. }
  212. },
  213. JSXFragment(node) {
  214. if (!checkFragmentShorthand || isWithinChildrenToArray) {
  215. return;
  216. }
  217. if (node.parent.type === 'ArrayExpression') {
  218. report(context, messages.missingArrayKeyUsePrag, 'missingArrayKeyUsePrag', {
  219. node,
  220. data: {
  221. reactPrag: reactPragma,
  222. fragPrag: fragmentPragma,
  223. },
  224. });
  225. }
  226. },
  227. // Array.prototype.map
  228. // eslint-disable-next-line no-multi-str
  229. 'CallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
  230. CallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"],\
  231. OptionalCallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
  232. OptionalCallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"]'(node) {
  233. if (isWithinChildrenToArray) {
  234. return;
  235. }
  236. const fn = node.arguments.length > 0 && node.arguments[0];
  237. if (!fn || !astUtil.isFunctionLikeExpression(fn)) {
  238. return;
  239. }
  240. checkArrowFunctionWithJSX(fn);
  241. checkFunctionsBlockStatement(fn);
  242. },
  243. // Array.from
  244. 'CallExpression[callee.type="MemberExpression"][callee.property.name="from"]'(node) {
  245. if (isWithinChildrenToArray) {
  246. return;
  247. }
  248. const fn = node.arguments.length > 1 && node.arguments[1];
  249. if (!astUtil.isFunctionLikeExpression(fn)) {
  250. return;
  251. }
  252. checkArrowFunctionWithJSX(fn);
  253. checkFunctionsBlockStatement(fn);
  254. },
  255. };
  256. },
  257. };