jsx-key.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  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. if (isArrFn && (node.body.type === 'JSXElement' || node.body.type === 'JSXFragment')) {
  137. checkIteratorElement(node.body);
  138. }
  139. }
  140. const seen = new WeakSet();
  141. return {
  142. 'ArrayExpression, JSXElement > JSXElement'(node) {
  143. const jsx = (node.type === 'ArrayExpression' ? node.elements : node.parent.children).filter((x) => x && x.type === 'JSXElement');
  144. if (jsx.length === 0) {
  145. return;
  146. }
  147. const map = {};
  148. jsx.forEach((element) => {
  149. const attrs = element.openingElement.attributes;
  150. const keys = attrs.filter((x) => x.name && x.name.name === 'key');
  151. if (keys.length === 0) {
  152. if (node.type === 'ArrayExpression') {
  153. report(context, messages.missingArrayKey, 'missingArrayKey', {
  154. node: element,
  155. });
  156. }
  157. } else {
  158. keys.forEach((attr) => {
  159. const value = context.getSourceCode().getText(attr.value);
  160. if (!map[value]) { map[value] = []; }
  161. map[value].push(attr);
  162. if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) {
  163. report(context, messages.keyBeforeSpread, 'keyBeforeSpread', {
  164. node: node.type === 'ArrayExpression' ? node : node.parent,
  165. });
  166. }
  167. });
  168. }
  169. });
  170. if (warnOnDuplicates) {
  171. values(map).filter((v) => v.length > 1).forEach((v) => {
  172. v.forEach((n) => {
  173. if (!seen.has(n)) {
  174. seen.add(n);
  175. report(context, messages.nonUniqueKeys, 'nonUniqueKeys', {
  176. node: n,
  177. });
  178. }
  179. });
  180. });
  181. }
  182. },
  183. JSXFragment(node) {
  184. if (!checkFragmentShorthand) {
  185. return;
  186. }
  187. if (node.parent.type === 'ArrayExpression') {
  188. report(context, messages.missingArrayKeyUsePrag, 'missingArrayKeyUsePrag', {
  189. node,
  190. data: {
  191. reactPrag: reactPragma,
  192. fragPrag: fragmentPragma,
  193. },
  194. });
  195. }
  196. },
  197. // Array.prototype.map
  198. // eslint-disable-next-line no-multi-str
  199. 'CallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
  200. CallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"],\
  201. OptionalCallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
  202. OptionalCallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"]'(node) {
  203. const fn = node.arguments[0];
  204. if (!astUtil.isFunctionLikeExpression(fn)) {
  205. return;
  206. }
  207. checkArrowFunctionWithJSX(fn);
  208. checkFunctionsBlockStatement(fn);
  209. },
  210. // Array.from
  211. 'CallExpression[callee.type="MemberExpression"][callee.property.name="from"]'(node) {
  212. const fn = node.arguments.length > 1 && node.arguments[1];
  213. if (!astUtil.isFunctionLikeExpression(fn)) {
  214. return;
  215. }
  216. checkArrowFunctionWithJSX(fn);
  217. checkFunctionsBlockStatement(fn);
  218. },
  219. };
  220. },
  221. };