diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js index 3bf496edb2..13d05b4748 100644 --- a/lib/rules/jsx-key.js +++ b/lib/rules/jsx-key.js @@ -6,6 +6,7 @@ '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'); @@ -14,7 +15,8 @@ const pragmaUtil = require('../util/pragma'); // ------------------------------------------------------------------------------ const defaultOptions = { - checkFragmentShorthand: false + checkFragmentShorthand: false, + checkKeyMustBeforeSpread: true }; module.exports = { @@ -31,6 +33,10 @@ module.exports = { checkFragmentShorthand: { type: 'boolean', default: defaultOptions.checkFragmentShorthand + }, + checkKeyMustBeforeSpread: { + type: 'boolean', + default: defaultOptions.checkKeyMustBeforeSpread } }, additionalProperties: false @@ -40,6 +46,7 @@ module.exports = { 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); @@ -61,9 +68,34 @@ module.exports = { return body.filter((item) => item.type === 'ReturnStatement')[0]; } + function isKeyAfterSpread(attributes) { + let keyAttributeIndex = -1; + let spreadAttributeIndex = -1; + + attributes.forEach((attribute, i) => { + if (attribute.type === 'JSXSpreadAttribute') { + spreadAttributeIndex = i; + } + + if (attribute.type === 'JSXAttribute') { + if (propName(attribute) === 'key') { + keyAttributeIndex = i; + } + } + }); + + return spreadAttributeIndex !== -1 && keyAttributeIndex > spreadAttributeIndex; + } + return { JSXElement(node) { if (hasProp(node.openingElement.attributes, 'key')) { + if (checkKeyMustBeforeSpread && isKeyAfterSpread(node.openingElement.attributes)) { + context.report({ + node, + message: '"key" prop must before "...spread"' + }); + } return; } diff --git a/tests/lib/rules/jsx-key.js b/tests/lib/rules/jsx-key.js index 49632f59e9..329a51e0fc 100644 --- a/tests/lib/rules/jsx-key.js +++ b/tests/lib/rules/jsx-key.js @@ -48,7 +48,9 @@ ruleTester.run('jsx-key', rule, { {code: '[1, 2, 3].map(function(x) { return; });'}, {code: 'foo(() =>
);'}, {code: 'foo(() => <>);', parser: parsers.BABEL_ESLINT}, - {code: '<>;', parser: parsers.BABEL_ESLINT} + {code: '<>;', parser: parsers.BABEL_ESLINT}, + {code: ';', parser: parsers.BABEL_ESLINT}, + {code: ';', parser: parsers.BABEL_ESLINT} ], invalid: [].concat({ code: '[];', @@ -88,5 +90,11 @@ ruleTester.run('jsx-key', rule, { options: [{checkFragmentShorthand: true}], settings, errors: [{message: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use Act.Frag instead'}] + }, { + code: '[];', + parser: parsers.BABEL_ESLINT, + options: [{checkKeyMustBeforeSpread: true}], + settings, + errors: [{message: '"key" prop must before "...spread"'}] }) });