You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
5.5 KiB
226 lines
5.5 KiB
/** |
|
* @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(); |
|
} |
|
}; |
|
} |
|
};
|
|
|