eslint-plugin-lodash
Version:
Lodash specific linting rules for ESLint
334 lines (295 loc) • 12.6 kB
JavaScript
'use strict';
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var _ = require('lodash');
var methodDataUtil = require('./methodDataUtil');
var astUtil = require('./astUtil');
var settingsUtil = require('./settingsUtil');
var contextsWithLodash = new WeakMap();
function getNameFromCjsRequire(init) {
if (_.get(init, 'callee.name') === 'require' && _.get(init, 'arguments.length') === 1 && init.arguments[0].type === 'Literal') {
return init.arguments[0].value;
}
}
function getLodashImportVisitors(context) {
return {
ImportDeclaration: function ImportDeclaration(_ref) {
var source = _ref.source;
var specifiers = _ref.specifiers;
if (source.value === 'lodash') {
contextsWithLodash.set(context, _.reduce(specifiers, function (_ref2, spec) {
var general = _ref2.general;
var methods = _ref2.methods;
switch (spec.type) {
case 'ImportNamespaceSpecifier':
case 'ImportDefaultSpecifier':
return { general: _.assign(general, _defineProperty({}, spec.local.name, true)), methods: methods };
case 'ImportSpecifier':
return { general: general, methods: _.assign(methods, _defineProperty({}, spec.local.name, spec.imported.name)) };
}
}, contextsWithLodash.get(context) || {}));
} else {
var match = /^lodash\/(\w+)/.exec(source.value);
if (match) {
var _ref3 = contextsWithLodash.get(context) || {};
var general = _ref3.general;
var methods = _ref3.methods;
contextsWithLodash.set(context, {
general: general,
methods: _.assign(methods, _defineProperty({}, _.get(specifiers, '[0].local.name'), match[1]))
});
}
}
},
VariableDeclarator: function VariableDeclarator(_ref4) {
var init = _ref4.init;
var id = _ref4.id;
var required = getNameFromCjsRequire(init);
if (required === 'lodash') {
if (id.type === 'Identifier') {
var _ref5 = contextsWithLodash.get(context) || {};
var general = _ref5.general;
var methods = _ref5.methods;
contextsWithLodash.set(context, { general: _.assign(general, _defineProperty({}, id.name, true), methods) });
} else if (id.type === 'ObjectPattern') {
var allImports = contextsWithLodash.get(context) || {};
var imports = _.reduce(id.properties, function (_ref6, prop) {
var general = _ref6.general;
var methods = _ref6.methods;
return { general: general, methods: _.assign(methods, _defineProperty({}, prop.value.name, prop.key.name)) };
}, allImports);
contextsWithLodash.set(context, imports);
}
} else if (required) {
var match = /^lodash\/(\w+)/.exec(required);
if (match) {
var _ref7 = contextsWithLodash.get(context) || {};
var _general = _ref7.general;
var _methods = _ref7.methods;
contextsWithLodash.set(context, {
general: _general,
methods: _.assign(_methods, _defineProperty({}, id.name, match[1]))
});
}
}
}
};
}
function isImportedLodash(node, context) {
if (context && node && node.type === 'Identifier') {
var contextData = contextsWithLodash.get(context);
return _.get(contextData, ['general', node.name]);
}
}
/**
* Returns whether the node is a lodash call with the specified pragma
* @param {Object} node
* @param {string} pragma
* @returns {boolean}
*/
function isLodashCall(node, pragma, context) {
return pragma && astUtil.isCallFromObject(node, pragma) || isImportedLodash(astUtil.getCaller(node), context);
}
/**
* Returns whether or not a node is a chainable method call in the specified version
* @param {Object} node
* @param {number} version
* @returns {boolean}
*/
function isChainable(node, version) {
return _.includes(methodDataUtil.getChainableAliases(version), astUtil.getMethodName(node));
}
/**
* Returns whether the node is an implicit chain start, '_(obj)...'
* @param {Object} node
* @param {string} pragma
* @returns {boolean}
*/
function isImplicitChainStart(node, pragma, context) {
return pragma && node.callee.name === pragma || isImportedLodash(node.callee, context);
}
/**
* Returns whether the node is an explicit chain start, '_.chain(obj)...'
* @param {Object} node
* @param {string} pragma
* @returns {boolean}
*/
function isExplicitChainStart(node, pragma, context) {
return isLodashCall(node, pragma, context) && astUtil.getMethodName(node) === 'chain';
}
/**
* Returns whether the node specified is a chain start, implicit or explicit
* @param {Object} node
* @param {string} pragma
* @returns {undefined|boolean}
*/
function isLodashChainStart(node, pragma, context) {
return node && node.type === 'CallExpression' && (isImplicitChainStart(node, pragma, context) || isExplicitChainStart(node, pragma, context));
}
/**
* Returns whehter the node is a chain breaker method in the specified version
* @param {Object} node
* @param {number} version
* @returns {boolean}
*/
function isChainBreaker(node, version) {
return methodDataUtil.isAliasOfMethod(version, 'value', astUtil.getMethodName(node));
}
/**
* Returns whether the node is a call to the specified method or one of its aliases in the version
* @param {Object} node
* @param {number} version
* @param {string} method
* @returns {boolean}
*/
function isCallToMethod(node, version, method) {
return methodDataUtil.isAliasOfMethod(version, method, astUtil.getMethodName(node));
}
/**
* Returns whether or not the node is a call to a lodash wrapper method
* @param {Object} node
* @param {number} version
* @returns {boolean}
*/
function isLodashWrapperMethod(node, version) {
return _.includes(methodDataUtil.getWrapperMethods(version), astUtil.getMethodName(node)) && node.type === 'CallExpression';
}
/**
* Gets the 'isX' method for a specified type, e.g. isObject
* @param {string} name
* @returns {string|null}
*/
function getIsTypeMethod(name) {
var types = ['number', 'boolean', 'function', 'Function', 'string', 'object', 'undefined', 'Date', 'Array', 'Error', 'Element'];
return _.includes(types, name) ? 'is' + _.capitalize(name) : null;
}
/**
* Returns whether or not the node is a call to a native collection method
* @param {Object} node
* @returns {boolean}
*/
function isNativeCollectionMethodCall(node) {
return _.includes(['every', 'fill', 'filter', 'find', 'findIndex', 'forEach', 'includes', 'map', 'reduce', 'reduceRight', 'some'], astUtil.getMethodName(node));
}
function getImportedLodashMethod(context, node) {
var contextData = contextsWithLodash.get(context);
if (!astUtil.isMethodCall(node) && contextData && contextData.methods) {
return contextData.methods[node.callee.name];
}
}
/**
* Gets the context's Lodash settings and a function and returns a visitor that calls the function for every Lodash or chain call
* @param {RuleContext} context
* @param {LodashReporter} reporter
* @returns {NodeTypeVisitor}
*/
function getLodashMethodCallExpVisitor(context, reporter) {
var _require$getSettings = require('./settingsUtil').getSettings(context);
var pragma = _require$getSettings.pragma;
var version = _require$getSettings.version;
return function (node) {
var iterateeIndex = void 0;
if (isLodashChainStart(node, pragma, context)) {
var prevNode = node;
node = node.parent.parent;
while (astUtil.getCaller(node) === prevNode && astUtil.isMethodCall(node) && !isChainBreaker(node, version)) {
var method = astUtil.getMethodName(node);
iterateeIndex = methodDataUtil.getIterateeIndex(version, method);
reporter(node, node.arguments[iterateeIndex - 1], { callType: 'chained', method: method, version: version });
prevNode = node;
node = node.parent.parent;
}
} else if (isLodashCall(node, pragma, context)) {
var _method = astUtil.getMethodName(node);
iterateeIndex = methodDataUtil.getIterateeIndex(version, _method);
reporter(node, node.arguments[iterateeIndex], { callType: 'method', method: _method, version: version });
} else if (version !== 3) {
var _method2 = getImportedLodashMethod(context, node);
if (_method2) {
iterateeIndex = methodDataUtil.getIterateeIndex(version, _method2);
reporter(node, node.arguments[iterateeIndex], { method: _method2, callType: 'single', version: version });
}
}
};
}
/**
* Returns whether the node's method call supports using shorthands in the specified version
* @param {Number} version
* @param {object} node
* @returns {boolean}
*/
function methodSupportsShorthand(version, method) {
return _.includes(methodDataUtil.getShorthandMethods(version), method);
}
function isLodashCallToMethod(node, settings, method, context) {
return isLodashCall(node, settings.pragma, context) && isCallToMethod(node, settings.version, method);
}
function isCallToLodashMethod(node, method, context) {
if (!node) {
return false;
}
var settings = settingsUtil.getSettings(context);
return isLodashCallToMethod(node, settings, method, context) || methodDataUtil.isAliasOfMethod(settings.version, method, getImportedLodashMethod(context, node));
}
function getLodashMethodVisitors(context, lodashCallExpVisitor) {
var visitors = getLodashImportVisitors(context);
visitors.CallExpression = getLodashMethodCallExpVisitor(context, lodashCallExpVisitor);
return visitors;
}
function getShorthandVisitors(context, checks, messages) {
var importVisitors = getLodashImportVisitors(context);
importVisitors.CallExpression = getLodashMethodCallExpVisitor(context, {
always: function always(node, iteratee, _ref8) {
var method = _ref8.method;
var version = _ref8.version;
if (methodSupportsShorthand(version, method) && checks.canUseShorthand(iteratee)) {
context.report(iteratee, messages.always);
}
},
never: function never(node, iteratee, _ref9) {
var method = _ref9.method;
if (checks.usesShorthand(node, iteratee, method)) {
context.report(iteratee || node.callee.property, messages.never);
}
}
}[context.options[0] || 'always']);
return importVisitors;
}
module.exports = {
isLodashCall: isLodashCall,
isLodashChainStart: isLodashChainStart,
isChainable: isChainable,
isChainBreaker: isChainBreaker,
isCallToMethod: isCallToMethod,
isLodashWrapperMethod: isLodashWrapperMethod,
getIsTypeMethod: getIsTypeMethod,
isNativeCollectionMethodCall: isNativeCollectionMethodCall,
isImplicitChainStart: isImplicitChainStart,
isExplicitChainStart: isExplicitChainStart,
getLodashMethodCallExpVisitor: getLodashMethodCallExpVisitor,
methodSupportsShorthand: methodSupportsShorthand,
getImportedLodashMethod: getImportedLodashMethod,
isCallToLodashMethod: isCallToLodashMethod,
getLodashImportVisitors: getLodashImportVisitors,
getShorthandVisitors: getShorthandVisitors,
getLodashMethodVisitors: getLodashMethodVisitors
};
/**
@callback LodashReporter
@param {Object} node
@param {Object} iteratee
@param {Object?} options
*/
/**
@callback NodeTypeVisitor
@param {Object} node
*/
/**
* @typedef {Object} ShorthandChecks
* @property {function} canUseShorthand
* @property {function} usesShorthand
*/
/**
* @typedef {object} ShorthandMessages
* @property {string} always
* @property {string} never
*/