eslint-plugin-lodash
Version:
Lodash specific linting rules for ESLint
354 lines (322 loc) • 10.4 kB
JavaScript
'use strict'
const _ = require('lodash')
/**
* Gets the object that called the method in a CallExpression
* @param {Object} node
* @returns {Object|undefined}
*/
const getCaller = _.property(['callee', 'object'])
/**
* Gets the name of a method in a CallExpression
* @param {Object} node
* @returns {string|undefined}
*/
const getMethodName = _.property(['callee', 'property', 'name'])
/**
* Returns whether the node is a method call
* @param {Object} node
* @returns {boolean}
*/
const isMethodCall = _.matches({type: 'CallExpression', callee: {type: 'MemberExpression'}})
const isFunctionExpression = _.overSome(
_.matchesProperty('type', 'FunctionExpression'),
_.matchesProperty('type', 'FunctionDeclaration')
)
/**
* Returns whether the node is a function declaration that has a block
* @param {Object} node
* @returns {boolean}
*/
const isFunctionDefinitionWithBlock = _.overSome(
isFunctionExpression,
_.matches({type: 'ArrowFunctionExpression', body: {type: 'BlockStatement'}})
)
/**
* If the node specified is a function, returns the node corresponding with the first statement/expression in that function
* @param {Object} node
* @returns {node|undefined}
*/
const getFirstFunctionLine = _.cond([
[isFunctionDefinitionWithBlock, _.property(['body', 'body', 0])],
[_.matches({type: 'ArrowFunctionExpression'}), _.property('body')]
])
/**
*
* @param {Object} node
* @returns {boolean|undefined}
*/
const isPropAccess = _.overSome(_.matches({computed: false}), _.matchesProperty(['property', 'type'], 'Literal'))
/**
* Returns whether the node is a member expression starting with the same object, up to the specified length
* @param {Object} node
* @param {string} objectName
* @param {Object} [options]
* @param {number} [options.maxLength]
* @param {boolean} [options.allowComputed]
* @returns {boolean|undefined}
*/
function isMemberExpOf(node, objectName, {maxLength = Number.MAX_VALUE, allowComputed} = {}) {
if (objectName) {
let curr = node
let depth = maxLength
while (curr && depth) {
if (allowComputed || isPropAccess(curr)) {
if (curr.type === 'MemberExpression' && curr.object.name === objectName) {
return true
}
curr = curr.object
depth--
} else {
return false
}
}
}
}
/**
* Returns the name of the first parameter of a function, if it exists
* @param {Object} func
* @returns {string|undefined}
*/
const getFirstParamName = _.property(['params', 0, 'name'])
/**
* Returns whether or not the expression is a return statement
* @param {Object} exp
* @returns {boolean|undefined}
*/
const isReturnStatement = _.matchesProperty('type', 'ReturnStatement')
/**
* Returns whether the node specified has only one statement
* @param {Object} func
* @returns {boolean}
*/
function hasOnlyOneStatement(func) {
if (isFunctionDefinitionWithBlock(func)) {
return _.get(func, 'body.body.length') === 1
}
if (func.type === 'ArrowFunctionExpression') {
return !_.get(func, 'body.body')
}
}
/**
* Returns whether the node is an object of a method call
* @param {Object} node
* @returns {boolean}
*/
function isObjectOfMethodCall(node) {
return _.get(node, 'parent.object') === node && _.get(node, 'parent.parent.type') === 'CallExpression'
}
/**
* Returns whether the node is a literal
* @param {Object} node
* @returns {boolean}
*/
function isLiteral(node) {
return node.type === 'Literal'
}
/**
* Returns whether the expression specified is a binary expression with the specified operator and one of its sides is a member expression of the specified object name
* @param {string} operator
* @param {Object} exp
* @param {string} objectName
* @param {number} maxLength
* @param {boolean} allowComputed
* @param {boolean} onlyLiterals
* @returns {boolean|undefined}
*/
function isBinaryExpWithMemberOf(operator, exp, objectName, {maxLength, allowComputed, onlyLiterals} = {}) {
if (!_.isMatch(exp, {type: 'BinaryExpression', operator})) {
return false
}
const [left, right] = [exp.left, exp.right].map(side => isMemberExpOf(side, objectName, {maxLength, allowComputed}))
return (left === !right) && (!onlyLiterals || isLiteral(exp.left) || isLiteral(exp.right))
}
/**
* Returns whether the specified expression is a negation.
* @param {Object} exp
* @returns {boolean|undefined}
*/
const isNegationExpression = _.matches({type: 'UnaryExpression', operator: '!'})
/**
* Returns whether the expression is a negation of a member of objectName, in the specified depth.
* @param {Object} exp
* @param {string} objectName
* @param {number} maxLength
* @returns {boolean|undefined}
*/
function isNegationOfMemberOf(exp, objectName, {maxLength} = {}) {
return isNegationExpression(exp) && isMemberExpOf(exp.argument, objectName, {maxLength, allowComputed: false})
}
/**
*
* @param {Object} exp
* @param {string} paramName
* @returns {boolean|undefined}
*/
function isIdentifierWithName(exp, paramName) {
return exp && paramName && exp.type === 'Identifier' && exp.name === paramName
}
/**
* Returns the node of the value returned in the first line, if any
* @param {Object} func
* @returns {Object|undefined}
*/
function getValueReturnedInFirstStatement(func) {
const firstLine = getFirstFunctionLine(func)
if (func) {
if (isFunctionDefinitionWithBlock(func)) {
return isReturnStatement(firstLine) ? firstLine.argument : undefined
}
if (func.type === 'ArrowFunctionExpression') {
return firstLine
}
}
}
/**
* Returns whether the node is a call from the specified object name
* @param {Object} node
* @param {string} objName
* @returns {boolean|undefined}
*/
function isCallFromObject(node, objName) {
return node && objName && node.type === 'CallExpression' && _.get(node, 'callee.object.name') === objName
}
/**
* Returns whether the node is actually computed (x['ab'] does not count, x['a' + 'b'] does
* @param {Object} node
* @returns {boolean|undefined}
*/
function isComputed(node) {
return _.get(node, 'computed') && node.property.type !== 'Literal'
}
/**
* Returns whether the two expressions refer to the same object (e.g. a['b'].c and a.b.c)
* @param {Object} a
* @param {Object} b
* @returns {boolean}
*/
function isEquivalentMemberExp(a, b) {
return _.isEqualWith(a, b, (left, right, key) => {
if (_.includes(['loc', 'range', 'computed', 'start', 'end', 'parent'], key)) {
return true
}
if (isComputed(left) || isComputed(right)) {
return false
}
if (key === 'property') {
const leftValue = left.name || left.value
const rightValue = right.name || right.value
return leftValue === rightValue
}
})
}
/**
* Returns whether the expression is a strict equality comparison, ===
* @param {Object} node
* @returns {boolean}
*/
const isEqEqEq = _.matches({type: 'BinaryExpression', operator: '==='})
const isMinus = node => node.type === 'UnaryExpression' && node.operator === '-'
/**
* Enum for type of comparison to int literal
* @readonly
* @enum {number}
*/
const comparisonType = {
exact: 0,
over: 1,
under: 2,
any: 3
}
const comparisonOperators = ['==', '!=', '===', '!==']
function getIsValue(value) {
return value < 0 ? _.overEvery(isMinus, _.matches({argument: {value: -value}})) : _.matches({value})
}
/**
* Returns the expression compared to the value in a binary expression, or undefined if there isn't one
* @param {Object} node
* @param {number} value
* @param {boolean} [checkOver=false]
* @returns {Object|undefined}
*/
function getExpressionComparedToInt(node, value, checkOver) {
const isValue = getIsValue(value)
if (_.includes(comparisonOperators, node.operator)) {
if (isValue(node.right)) {
return node.left
}
if (isValue(node.left)) {
return node.right
}
}
if (checkOver) {
if (node.operator === '>' && isValue(node.right)) {
return node.left
}
if (node.operator === '<' && isValue(node.left)) {
return node.right
}
const isNext = getIsValue(value + 1)
if ((node.operator === '>=' || node.operator === '<') && isNext(node.right)) {
return node.left
}
if ((node.operator === '<=' || node.operator === '>') && isNext(node.left)) {
return node.right
}
}
}
/**
* Returns whether the node is a call to indexOf
* @param {Object} node
* @returns {boolean}
*/
const isIndexOfCall = node => isMethodCall(node) && getMethodName(node) === 'indexOf'
/**
* Returns whether the node is a call to findIndex
* @param {Object} node
* @returns {boolean}
*/
const isFindIndexCall = node => isMethodCall(node) && getMethodName(node) === 'findIndex'
/**
* Returns an array of identifier names returned in a parameter or variable definition
* @param node an AST node which is a parameter or variable declaration
* @returns {string[]} List of names defined in the parameter
*/
function collectParameterValues(node) {
switch (node && node.type) {
case 'Identifier':
return [node.name]
case 'ObjectPattern':
return _.flatMap(node.properties, prop => collectParameterValues(prop.value))
case 'ArrayPattern':
return _.flatMap(node.elements, collectParameterValues)
default:
return []
}
}
module.exports = {
getCaller,
getMethodName,
isMethodCall,
getFirstFunctionLine,
isMemberExpOf,
getFirstParamName,
hasOnlyOneStatement,
isObjectOfMethodCall,
isEqEqEqToMemberOf: isBinaryExpWithMemberOf.bind(null, '==='),
isNotEqEqToMemberOf: isBinaryExpWithMemberOf.bind(null, '!=='),
isNegationOfMemberOf,
isIdentifierWithName,
isNegationExpression,
getValueReturnedInFirstStatement,
isCallFromObject,
isComputed,
isEquivalentMemberExp,
isEqEqEq,
comparisonType,
getExpressionComparedToInt,
isIndexOfCall,
isFindIndexCall,
isFunctionExpression,
isFunctionDefinitionWithBlock,
collectParameterValues
}