| /** |
| * @fileoverview Prevent usage of Array index in keys |
| * @author Joe Lencioni |
| */ |
| |
| 'use strict'; |
| |
| const has = require('has'); |
| const astUtil = require('../util/ast'); |
| const docsUrl = require('../util/docsUrl'); |
| const pragma = require('../util/pragma'); |
| |
| // ------------------------------------------------------------------------------ |
| // Rule Definition |
| // ------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| docs: { |
| description: 'Prevent usage of Array index in keys', |
| category: 'Best Practices', |
| recommended: false, |
| url: docsUrl('no-array-index-key') |
| }, |
| |
| schema: [] |
| }, |
| |
| create(context) { |
| // -------------------------------------------------------------------------- |
| // Public |
| // -------------------------------------------------------------------------- |
| const indexParamNames = []; |
| const iteratorFunctionsToIndexParamPosition = { |
| every: 1, |
| filter: 1, |
| find: 1, |
| findIndex: 1, |
| forEach: 1, |
| map: 1, |
| reduce: 2, |
| reduceRight: 2, |
| some: 1 |
| }; |
| const ERROR_MESSAGE = 'Do not use Array index in keys'; |
| |
| function isArrayIndex(node) { |
| return node.type === 'Identifier' && |
| indexParamNames.indexOf(node.name) !== -1; |
| } |
| |
| function isUsingReactChildren(node) { |
| const callee = node.callee; |
| if ( |
| !callee || |
| !callee.property || |
| !callee.object |
| ) { |
| return null; |
| } |
| |
| const isReactChildMethod = ['map', 'forEach'].indexOf(callee.property.name) > -1; |
| if (!isReactChildMethod) { |
| return null; |
| } |
| |
| const obj = callee.object; |
| if (obj && obj.name === 'Children') { |
| return true; |
| } |
| if (obj && obj.object && obj.object.name === pragma.getFromContext(context)) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| function getMapIndexParamName(node) { |
| const callee = node.callee; |
| if (callee.type !== 'MemberExpression') { |
| return null; |
| } |
| if (callee.property.type !== 'Identifier') { |
| return null; |
| } |
| if (!has(iteratorFunctionsToIndexParamPosition, callee.property.name)) { |
| return null; |
| } |
| |
| const callbackArg = isUsingReactChildren(node) ? |
| node.arguments[1] : |
| node.arguments[0]; |
| |
| if (!callbackArg) { |
| return null; |
| } |
| |
| if (!astUtil.isFunctionLikeExpression(callbackArg)) { |
| return null; |
| } |
| |
| const params = callbackArg.params; |
| |
| const indexParamPosition = iteratorFunctionsToIndexParamPosition[callee.property.name]; |
| if (params.length < indexParamPosition + 1) { |
| return null; |
| } |
| |
| return params[indexParamPosition].name; |
| } |
| |
| function getIdentifiersFromBinaryExpression(side) { |
| if (side.type === 'Identifier') { |
| return side; |
| } |
| |
| if (side.type === 'BinaryExpression') { |
| // recurse |
| const left = getIdentifiersFromBinaryExpression(side.left); |
| const right = getIdentifiersFromBinaryExpression(side.right); |
| return [].concat(left, right).filter(Boolean); |
| } |
| |
| return null; |
| } |
| |
| function checkPropValue(node) { |
| if (isArrayIndex(node)) { |
| // key={bar} |
| context.report({ |
| node, |
| message: ERROR_MESSAGE |
| }); |
| return; |
| } |
| |
| if (node.type === 'TemplateLiteral') { |
| // key={`foo-${bar}`} |
| node.expressions.filter(isArrayIndex).forEach(() => { |
| context.report({node, message: ERROR_MESSAGE}); |
| }); |
| |
| return; |
| } |
| |
| if (node.type === 'BinaryExpression') { |
| // key={'foo' + bar} |
| const identifiers = getIdentifiersFromBinaryExpression(node); |
| |
| identifiers.filter(isArrayIndex).forEach(() => { |
| context.report({node, message: ERROR_MESSAGE}); |
| }); |
| } |
| } |
| |
| return { |
| CallExpression(node) { |
| if ( |
| node.callee && |
| node.callee.type === 'MemberExpression' && |
| ['createElement', 'cloneElement'].indexOf(node.callee.property.name) !== -1 && |
| node.arguments.length > 1 |
| ) { |
| // React.createElement |
| if (!indexParamNames.length) { |
| return; |
| } |
| |
| const props = node.arguments[1]; |
| |
| if (props.type !== 'ObjectExpression') { |
| return; |
| } |
| |
| props.properties.forEach((prop) => { |
| if (!prop.key || prop.key.name !== 'key') { |
| // { ...foo } |
| // { foo: bar } |
| return; |
| } |
| |
| checkPropValue(prop.value); |
| }); |
| |
| return; |
| } |
| |
| const mapIndexParamName = getMapIndexParamName(node); |
| if (!mapIndexParamName) { |
| return; |
| } |
| |
| indexParamNames.push(mapIndexParamName); |
| }, |
| |
| JSXAttribute(node) { |
| if (node.name.name !== 'key') { |
| // foo={bar} |
| return; |
| } |
| |
| if (!indexParamNames.length) { |
| // Not inside a call expression that we think has an index param. |
| return; |
| } |
| |
| const value = node.value; |
| if (!value || value.type !== 'JSXExpressionContainer') { |
| // key='foo' or just simply 'key' |
| return; |
| } |
| |
| checkPropValue(value.expression); |
| }, |
| |
| 'CallExpression:exit': function (node) { |
| const mapIndexParamName = getMapIndexParamName(node); |
| if (!mapIndexParamName) { |
| return; |
| } |
| |
| indexParamNames.pop(); |
| } |
| }; |
| } |
| }; |