From 59a3b258085aa82718998e1b9239de358a8c1738 Mon Sep 17 00:00:00 2001 From: Omer Ganim Date: Thu, 24 Dec 2015 15:35:00 +0200 Subject: [PATCH] add rule chain-style --- README.md | 4 +- docs/rules/chain-style.md | 79 ++++++++++++++++++++++++++++++++++ lib/rules/chain-style.js | 54 +++++++++++++++++++++++ tests/lib/rules/chain-style.js | 53 +++++++++++++++++++++++ 4 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 docs/rules/chain-style.md create mode 100644 lib/rules/chain-style.js create mode 100644 tests/lib/rules/chain-style.js diff --git a/README.md b/README.md index 2db7ffa..8779e94 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ Finally, enable all of the rules that you would like to use. "lodash3/prefer-times": 1, "lodash3/prefer-startswith": 1, "lodash3/prefer-noop": 1, - "lodash3/prefer-constant": 1 + "lodash3/prefer-constant": 1, + "lodash3/chain-style": [1, "as-needed"] } } ``` @@ -91,6 +92,7 @@ Finally, enable all of the rules that you would like to use. * [prefer-startswth](docs/rules/prefer-startswith.md): Prefer `_.startsWith` over `a.indexOf(b) === 0`. * [prefer-noop](docs/rules/prefer-noop.md): Prefer `_.noop` over empty functions. * [prefer-constant](docs/rules/prefer-constant.md): Prefer `_.constant` over functions returning literals. +* [chain-style](docs/rules/chain-style.md): Enforce a specific chain style: explicit, implicit, or explicit only when necessary. # License diff --git a/docs/rules/chain-style.md b/docs/rules/chain-style.md new file mode 100644 index 0000000..1accc90 --- /dev/null +++ b/docs/rules/chain-style.md @@ -0,0 +1,79 @@ +# Chain Style + +There are two ways to create a lodash chain: implicit chaining and explicit chaining with the `_.chain` method. + +To use implicit chaining, you can call `_(value)` on your value and, and then finish the chain with the `.value()` method or with any method that returns a single value (e.g. `.first()`, `.max()`) +For example: +```js +var maxFiltered = _(arr).filter(someFilter).max(someCallback); +``` +In order to keep the value wrapped in the chain after any single-value method, chaining needs to be explicit, with the `_.chain()` method. +For example: +```js +var mergedFilteredMax = _.chain(arr).filter(someFilter).max(someCallback).assign(obj).value(); +``` + +For more information, check out the [Lodash documentation for chaining](https://lodash.com/docs#_). + +## Rule Details + +This rule takes one argument, the preferred style: `implicit`, `explicit` or `as-needed`. (default is `as-needed`). + +The following patterns are considered problems: + +```js +/*eslint lodash3/chain-style: [2, "as-needed"]*/ + +_.chain(val).map(f).filter(g).value(); // Unnecessary explicit chaining + +_.chain(val).map(f).join(c).value(); // Unnecessary explicit chaining, the chain-breaking method join() is last in the chain. + +``` + +```js +/*eslint lodash3/chain-style: [2, "implicit"]*/ + +_.chain(val).map(f).filter(g).value(); // Do not use explicit chaining + +_.chain(val).map(f).first().assign(obj).value(); // Do not use explicit chaining + +``` + +```js +/*eslint lodash3/chain-style: [2, "explicit"]*/ + +_(val).map(f).filter(g).value(); // Do not use implicit chaining + + +``` + + + +The following patterns are not considered warnings: + +```js +/*eslint lodash3/chain-style: [2, "as-needed"]*/ + +_(val).map(f).filter(g).value(); + +_.chain(val).map(f).first().assign(obj).value(); + +``` + +```js +/*eslint lodash3/chain-style: [2, "implicit"]*/ + +_(val).map(f).filter(g).value(); + +``` + +```js +/*eslint lodash3/chain-style: [2, "explicit"]*/ + +_.chain(val).map(f).filter(g).value(); + +``` + +## When Not To Use It + +If you do not want to enforce a specific chain style, then you can disable this rule. diff --git a/lib/rules/chain-style.js b/lib/rules/chain-style.js new file mode 100644 index 0000000..7aad7da --- /dev/null +++ b/lib/rules/chain-style.js @@ -0,0 +1,54 @@ +/** + * @fileoverview Rule to enforce a specific chain style + */ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +module.exports = function (context) { + var lodashUtil = require('../util/lodashUtil'); + var astUtil = require('../util/astUtil'); + + function isExplicitChainStart(node) { + return lodashUtil.isLodashCall(node) && lodashUtil.isCallToMethod(node, 'chain'); + } + + var callExpressionVisitors = { + 'as-needed': function (node) { + if (isExplicitChainStart(node)) { + var curr = node.parent.parent; + var needed = false; + while (astUtil.isMethodCall(curr) && !lodashUtil.isChainBreaker(curr)) { + if (!lodashUtil.isChainable(curr) && !lodashUtil.isChainBreaker(curr.parent.parent)) { + needed = true; + } + curr = curr.parent.parent; + } + if (astUtil.isMethodCall(curr) && !needed) { + context.report(node, 'Unnecessary explicit chaining'); + } + } + }, + implicit: function (node) { + if (isExplicitChainStart(node)) { + context.report(node, 'Do not use explicit chaining'); + } + }, + explicit: function (node) { + if (node.callee.name === '_') { + context.report(node, 'Do not use implicit chaining'); + } + } + }; + + return { + CallExpression: callExpressionVisitors[context.options[0] || 'as-needed'] + }; +}; + +module.exports.schema = [ + { + enum: ['as-needed', 'implicit', 'explicit'] + } +]; \ No newline at end of file diff --git a/tests/lib/rules/chain-style.js b/tests/lib/rules/chain-style.js new file mode 100644 index 0000000..066a332 --- /dev/null +++ b/tests/lib/rules/chain-style.js @@ -0,0 +1,53 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../../../lib/rules/chain-style'); +var RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var ruleTester = new RuleTester(); +var errors = { + asNeeded: [{message: 'Unnecessary explicit chaining'}], + implicit: [{message: 'Do not use explicit chaining'}], + explicit: [{message: 'Do not use implicit chaining'}] +}; + +ruleTester.run('chain-style', rule, { + valid: [ + '_(a).map(f).filter(g).value()', + '_(a).map(f).join(" ")', + '_.chain(a).map(f).first().assign(obj).value()', + {code: '_(a).map(f).filter(g).value();', options: ['as-needed']}, + {code: '_(a).map(f).join(" ")', options: ['implicit']}, + {code: '_.chain(a).map(f).filter(b).value()', options: ['explicit']} + ], + invalid: [{ + code: '_.chain(a).map(f).filter(g).value()', + errors: errors.asNeeded + }, { + code: '_.chain(a).map(f).max(g).value()', + errors: errors.asNeeded + }, { + code: '_.chain(a).map(f).filter(g).value()', + errors: errors.asNeeded, + options: ['as-needed'] + }, { + code: '_(a).map(f).max(g)', + errors: errors.explicit, + options: ['explicit'] + }, { + code: '_.chain(a).map(f).max(g).assign(obj).value()', + errors: errors.implicit, + options: ['implicit'] + }, { + code: '_.chain(a).map(f).max(g).value()', + errors: errors.implicit, + options: ['implicit'] + }] +});