UNPKG

eslint-plugin-lodash

Version:

Lodash specific linting rules for ESLint

334 lines (295 loc) 12.6 kB
'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 */