diff --git a/README.md b/README.md index 55273f6..4dbd5f5 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,9 @@ Finally, enable all of the rules that you would like to use. "lodash3/prefer-map": 1, "lodash3/prefer-wrapper-method": 1, "lodash3/prefer-invoke": 1, - "lodash3/prefer-thru": 1 + "lodash3/prefer-thru": 1, + "lodash3/prefer-lodash-method": 1, + "lodash3/prefer-lodash-typecheck": 1 } } ``` @@ -67,6 +69,8 @@ Finally, enable all of the rules that you would like to use. * [prefer-wrapper-method](docs/rules/prefer-wrapper-method.md): Prefer using array and string methods in the chain and not the initial value, e.g. `_(str).split(' ')...` * [prefer-invoke](docs/rules/prefer-invoke.md): Prefer using `_.invoke` over `_.map` with a method call inside. * [prefer-thru](docs/rules/prefer-thru.md): Prefer using `_.prototype.thru` in the chain and not call functions in the initial value, e.g. `_(x).thru(f).map(g)...` +* [prefer-lodash-method](docs/rules/prefer-lodash-method.md): Prefer using Lodash collection methods (e.g. `_.map`) over native array methods. +* [prefer-lodash-typecheck](docs/rules/prefer-lodash-typecheck.md): Prefer using `_.is*` methods over `typeof` and `instanceof` checks when applicable. # License diff --git a/docs/rules/prefer-lodash-method.md b/docs/rules/prefer-lodash-method.md new file mode 100644 index 0000000..6ac05bd --- /dev/null +++ b/docs/rules/prefer-lodash-method.md @@ -0,0 +1,34 @@ +# Prefer Lodash method + +When using native functions like forEach and map, it's often better to use the Lodash implementation. + +## Rule Details + +This rule takes no arguments. + +The following patterns are considered warnings: + +```js + +var b = a.map(f); + +if (arr.some(f)) { + // ... +} + +``` + +The following patterns are not considered warnings: + +```js + +_.map(a, f); + + _(arr).map(f).reduce(g); + +``` + + +## When Not To Use It + +If you do not want to enforce using Lodash methods, you should not use this rule. diff --git a/docs/rules/prefer-lodash-typecheck.md b/docs/rules/prefer-lodash-typecheck.md new file mode 100644 index 0000000..fb138d5 --- /dev/null +++ b/docs/rules/prefer-lodash-typecheck.md @@ -0,0 +1,36 @@ +# Prefer Lodash typecheck + +Getting the specific type of a variable or expression can be done with `typeof` or `instanceof`. However, it's often more expressive to use the Lodash equivalent function + +## Rule Details + +This rule takes no arguments. + +The following patterns are considered warnings: + +```js + +if (typeof a === 'number') { + // ... +} + +var isNotString = typeof b !== 'string'; + +var isArray = a instanceof Array; + +``` + +The following patterns are not considered warnings: + +```js + +var areSameType = typeof a === typeof b; + +var isCar = truck instanceof Car; + +``` + + +## When Not To Use It + +If you do not want to enforce using Lodash methods for type checks, you should not use this rule. diff --git a/lib/rules/prefer-lodash-method.js b/lib/rules/prefer-lodash-method.js new file mode 100644 index 0000000..fecc932 --- /dev/null +++ b/lib/rules/prefer-lodash-method.js @@ -0,0 +1,23 @@ +/** + * @fileoverview Rule to check if there's a method in the chain start that can be in the chain + */ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + var lodashUtil = require('../util/lodashUtil'); + var astUtil = require('../util/astUtil'); + + var REPORT_MESSAGE = 'Prefer \'_.{{method}}\' over the native function.'; + + return { + CallExpression: function (node) { + if (!lodashUtil.isLodashCall(node) && !lodashUtil.isLodashWrapper(astUtil.getCaller(node)) && lodashUtil.canBeLodashMethod(astUtil.getMethodName(node))) { + context.report(node, REPORT_MESSAGE, {method: astUtil.getMethodName(node)}); + } + } + }; +}; diff --git a/lib/rules/prefer-lodash-typecheck.js b/lib/rules/prefer-lodash-typecheck.js new file mode 100644 index 0000000..7e8f50a --- /dev/null +++ b/lib/rules/prefer-lodash-typecheck.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Rule to check if there's a method in the chain start that can be in the chain + */ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + var lodashUtil = require('../util/lodashUtil'); + + function isTypeOf(node) { + return node && node.type === 'UnaryExpression' && node.operator === 'typeof'; + } + + function isLiteral(node) { + return node && node.type === 'Literal'; + } + + function isStrictComparison(node) { + return (node.operator === '===' || node.operator === '!=='); + } + + function isCompareTypeOfToLiteral(node) { + return isStrictComparison(node) && + ((isTypeOf(node.left) && isLiteral(node.right)) || (isTypeOf(node.right) && isLiteral(node.left))); + } + + var REPORT_MESSAGE = 'Prefer \'_.{{method}}\' over {{actual}}.'; + + return { + BinaryExpression: function (node) { + if (isCompareTypeOfToLiteral(node)) { + context.report(node, REPORT_MESSAGE, { + method: lodashUtil.getIsTypeMethod(isLiteral(node.left) ? node.left.value : node.right.value), + actual: '\'typeof\' comparison' + }); + } else if (node.operator === 'instanceof') { + var lodashEquivalent = lodashUtil.getIsTypeMethod(node.right.name); + if (node.right.type === 'Identifier' && lodashEquivalent) { + context.report(node, REPORT_MESSAGE, {method: lodashEquivalent, actual: '\'instanceof ' + node.right.name + '\''}); + } + } + } + }; +}; diff --git a/lib/util/lodashUtil.js b/lib/util/lodashUtil.js index b9813f6..1894f4c 100644 --- a/lib/util/lodashUtil.js +++ b/lib/util/lodashUtil.js @@ -54,7 +54,14 @@ function isCallToMethod(node, method) { function isLodashWrapperMethod(node) { return _.includes(aliasMap.WRAPPER_METHODS, astUtil.getMethodName(node)) && node.type === 'CallExpression'; } +function getIsTypeMethod(name) { + var types = ['number', 'boolean', 'function', 'Function', 'string', 'object', 'undefined', 'Date', 'Array', 'Error', 'Element']; + return _.includes(types, name) ? 'is' + _.capitalize(name) : null; +} +function canBeLodashMethod(name) { + return _.includes(['forEach', 'map', 'reduce', 'filter', 'every', 'some'], name); +} module.exports = { isLodashCall: isLodashCall, isLodashChainStart: isLodashChainStart, @@ -65,5 +72,7 @@ module.exports = { isChainBreaker: isChainBreaker, isExplicitMethodChaining: isExplicitMethodChaining, isCallToMethod: isCallToMethod, - isLodashWrapperMethod: isLodashWrapperMethod + isLodashWrapperMethod: isLodashWrapperMethod, + getIsTypeMethod: getIsTypeMethod, + canBeLodashMethod: canBeLodashMethod }; diff --git a/tests/lib/rules/prefer-lodash-method.js b/tests/lib/rules/prefer-lodash-method.js new file mode 100644 index 0000000..d3dbbb5 --- /dev/null +++ b/tests/lib/rules/prefer-lodash-method.js @@ -0,0 +1,32 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../../../lib/rules/prefer-lodash-method'); +var RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var ruleTester = new RuleTester(); +var errors = [{message: 'Prefer \'_.map\' over the native function.'}]; +ruleTester.run('prefer-lodash-method', rule, { + valid: [{ + code: 'var x = _.map(arr, f)' + }, { + code: 'var x = _(arr).map(f).reduce(g)' + }, { + code: 'var x = _.chain(arr).map(f).reduce(g).value()' + }], + invalid: [{ + code: 'var x = a.map(function(x) {return x.f()});', + errors: errors + }, { + code: 'var x = arr.map(x => x.f())', + ecmaFeatures: {arrowFunctions: true}, + errors: errors + }] +}); diff --git a/tests/lib/rules/prefer-lodash-typecheck.js b/tests/lib/rules/prefer-lodash-typecheck.js new file mode 100644 index 0000000..4ad1de2 --- /dev/null +++ b/tests/lib/rules/prefer-lodash-typecheck.js @@ -0,0 +1,37 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../../../lib/rules/prefer-lodash-typecheck'); +var RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var ruleTester = new RuleTester(); +var errors = { + typeof: [{message: 'Prefer \'_.isNumber\' over \'typeof\' comparison.'}], + instanceof: [{message: 'Prefer \'_.isArray\' over \'instanceof Array\'.'}] +}; +ruleTester.run('prefer-lodash-typecheck', rule, { + valid: [{ + code: 'var x = a instanceof B' + }, { + code: 'var x = a > b ? a : b' + }, { + code: 'var x = typeof a === typeof b' + }], + invalid: [{ + code: 'var x = typeof a === "number"', + errors: errors.typeof + }, { + code: 'var x = "number" !== typeof a', + errors: errors.typeof + }, { + code: 'var x = a instanceof Array', + errors: errors.instanceof + }] +});