123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- /**
- * @fileoverview Report missing `key` props in iterators/collection literals.
- * @author Ben Mosher
- */
- 'use strict';
- const hasProp = require('jsx-ast-utils/hasProp');
- const propName = require('jsx-ast-utils/propName');
- const docsUrl = require('../util/docsUrl');
- const pragmaUtil = require('../util/pragma');
- // ------------------------------------------------------------------------------
- // Rule Definition
- // ------------------------------------------------------------------------------
- const defaultOptions = {
- checkFragmentShorthand: false,
- checkKeyMustBeforeSpread: false
- };
- module.exports = {
- meta: {
- docs: {
- description: 'Report missing `key` props in iterators/collection literals',
- category: 'Possible Errors',
- recommended: true,
- url: docsUrl('jsx-key')
- },
- messages: {
- missingIterKey: 'Missing "key" prop for element in iterator',
- missingIterKeyUsePrag: 'Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
- missingArrayKey: 'Missing "key" prop for element in array',
- missingArrayKeyUsePrag: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
- 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`'
- },
- schema: [{
- type: 'object',
- properties: {
- checkFragmentShorthand: {
- type: 'boolean',
- default: defaultOptions.checkFragmentShorthand
- },
- checkKeyMustBeforeSpread: {
- type: 'boolean',
- default: defaultOptions.checkKeyMustBeforeSpread
- }
- },
- additionalProperties: false
- }]
- },
- create(context) {
- const options = Object.assign({}, defaultOptions, context.options[0]);
- const checkFragmentShorthand = options.checkFragmentShorthand;
- const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread;
- const reactPragma = pragmaUtil.getFromContext(context);
- const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
- function checkIteratorElement(node) {
- if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) {
- context.report({
- node,
- messageId: 'missingIterKey'
- });
- } else if (checkFragmentShorthand && node.type === 'JSXFragment') {
- context.report({
- node,
- messageId: 'missingIterKeyUsePrag',
- data: {
- reactPrag: reactPragma,
- fragPrag: fragmentPragma
- }
- });
- }
- }
- function getReturnStatement(body) {
- return body.filter((item) => item.type === 'ReturnStatement')[0];
- }
- function isKeyAfterSpread(attributes) {
- let hasFoundSpread = false;
- return attributes.some((attribute) => {
- if (attribute.type === 'JSXSpreadAttribute') {
- hasFoundSpread = true;
- return false;
- }
- if (attribute.type !== 'JSXAttribute') {
- return false;
- }
- return hasFoundSpread && propName(attribute) === 'key';
- });
- }
- return {
- JSXElement(node) {
- if (hasProp(node.openingElement.attributes, 'key')) {
- if (checkKeyMustBeforeSpread && isKeyAfterSpread(node.openingElement.attributes)) {
- context.report({
- node,
- messageId: 'keyBeforeSpread'
- });
- }
- return;
- }
- if (node.parent.type === 'ArrayExpression') {
- context.report({
- node,
- messageId: 'missingArrayKey'
- });
- }
- },
- JSXFragment(node) {
- if (!checkFragmentShorthand) {
- return;
- }
- if (node.parent.type === 'ArrayExpression') {
- context.report({
- node,
- messageId: 'missingArrayKeyUsePrag',
- data: {
- reactPrag: reactPragma,
- fragPrag: fragmentPragma
- }
- });
- }
- },
- // Array.prototype.map
- 'CallExpression, OptionalCallExpression'(node) {
- if (node.callee && node.callee.type !== 'MemberExpression' && node.callee.type !== 'OptionalMemberExpression') {
- return;
- }
- if (node.callee && node.callee.property && node.callee.property.name !== 'map') {
- return;
- }
- const fn = node.arguments[0];
- const isFn = fn && fn.type === 'FunctionExpression';
- const isArrFn = fn && fn.type === 'ArrowFunctionExpression';
- if (isArrFn && (fn.body.type === 'JSXElement' || fn.body.type === 'JSXFragment')) {
- checkIteratorElement(fn.body);
- }
- if (isFn || isArrFn) {
- if (fn.body.type === 'BlockStatement') {
- const returnStatement = getReturnStatement(fn.body.body);
- if (returnStatement && returnStatement.argument) {
- checkIteratorElement(returnStatement.argument);
- }
- }
- }
- }
- };
- }
- };
|