jsx-key.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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 docsUrl = require('../util/docsUrl');
  9. const pragmaUtil = require('../util/pragma');
  10. // ------------------------------------------------------------------------------
  11. // Rule Definition
  12. // ------------------------------------------------------------------------------
  13. const defaultOptions = {
  14. checkFragmentShorthand: false,
  15. checkKeyMustBeforeSpread: false
  16. };
  17. module.exports = {
  18. meta: {
  19. docs: {
  20. description: 'Report missing `key` props in iterators/collection literals',
  21. category: 'Possible Errors',
  22. recommended: true,
  23. url: docsUrl('jsx-key')
  24. },
  25. messages: {
  26. missingIterKey: 'Missing "key" prop for element in iterator',
  27. missingIterKeyUsePrag: 'Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
  28. missingArrayKey: 'Missing "key" prop for element in array',
  29. missingArrayKeyUsePrag: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
  30. 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`'
  31. },
  32. schema: [{
  33. type: 'object',
  34. properties: {
  35. checkFragmentShorthand: {
  36. type: 'boolean',
  37. default: defaultOptions.checkFragmentShorthand
  38. },
  39. checkKeyMustBeforeSpread: {
  40. type: 'boolean',
  41. default: defaultOptions.checkKeyMustBeforeSpread
  42. }
  43. },
  44. additionalProperties: false
  45. }]
  46. },
  47. create(context) {
  48. const options = Object.assign({}, defaultOptions, context.options[0]);
  49. const checkFragmentShorthand = options.checkFragmentShorthand;
  50. const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread;
  51. const reactPragma = pragmaUtil.getFromContext(context);
  52. const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
  53. function checkIteratorElement(node) {
  54. if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) {
  55. context.report({
  56. node,
  57. messageId: 'missingIterKey'
  58. });
  59. } else if (checkFragmentShorthand && node.type === 'JSXFragment') {
  60. context.report({
  61. node,
  62. messageId: 'missingIterKeyUsePrag',
  63. data: {
  64. reactPrag: reactPragma,
  65. fragPrag: fragmentPragma
  66. }
  67. });
  68. }
  69. }
  70. function getReturnStatement(body) {
  71. return body.filter((item) => item.type === 'ReturnStatement')[0];
  72. }
  73. function isKeyAfterSpread(attributes) {
  74. let hasFoundSpread = false;
  75. return attributes.some((attribute) => {
  76. if (attribute.type === 'JSXSpreadAttribute') {
  77. hasFoundSpread = true;
  78. return false;
  79. }
  80. if (attribute.type !== 'JSXAttribute') {
  81. return false;
  82. }
  83. return hasFoundSpread && propName(attribute) === 'key';
  84. });
  85. }
  86. return {
  87. JSXElement(node) {
  88. if (hasProp(node.openingElement.attributes, 'key')) {
  89. if (checkKeyMustBeforeSpread && isKeyAfterSpread(node.openingElement.attributes)) {
  90. context.report({
  91. node,
  92. messageId: 'keyBeforeSpread'
  93. });
  94. }
  95. return;
  96. }
  97. if (node.parent.type === 'ArrayExpression') {
  98. context.report({
  99. node,
  100. messageId: 'missingArrayKey'
  101. });
  102. }
  103. },
  104. JSXFragment(node) {
  105. if (!checkFragmentShorthand) {
  106. return;
  107. }
  108. if (node.parent.type === 'ArrayExpression') {
  109. context.report({
  110. node,
  111. messageId: 'missingArrayKeyUsePrag',
  112. data: {
  113. reactPrag: reactPragma,
  114. fragPrag: fragmentPragma
  115. }
  116. });
  117. }
  118. },
  119. // Array.prototype.map
  120. 'CallExpression, OptionalCallExpression'(node) {
  121. if (node.callee && node.callee.type !== 'MemberExpression' && node.callee.type !== 'OptionalMemberExpression') {
  122. return;
  123. }
  124. if (node.callee && node.callee.property && node.callee.property.name !== 'map') {
  125. return;
  126. }
  127. const fn = node.arguments[0];
  128. const isFn = fn && fn.type === 'FunctionExpression';
  129. const isArrFn = fn && fn.type === 'ArrowFunctionExpression';
  130. if (isArrFn && (fn.body.type === 'JSXElement' || fn.body.type === 'JSXFragment')) {
  131. checkIteratorElement(fn.body);
  132. }
  133. if (isFn || isArrFn) {
  134. if (fn.body.type === 'BlockStatement') {
  135. const returnStatement = getReturnStatement(fn.body.body);
  136. if (returnStatement && returnStatement.argument) {
  137. checkIteratorElement(returnStatement.argument);
  138. }
  139. }
  140. }
  141. }
  142. };
  143. }
  144. };