Skip to content
Permalink
Browse files

New: Rule to enforce newline after each call in the chain (fixes #4538)

Added new rule (`newline-per-chained-call`) to enforce each call on a new line when chaining the calls.
  • Loading branch information...
rpatil26 committed Jan 16, 2016
1 parent 6433b7b commit b2aedfe58c3569ff8e9caf132ad625cecac0308c
@@ -164,7 +164,8 @@
"new-cap": 0,
"new-parens": 0,
"newline-after-var": 0,
"object-curly-spacing": 0,
"newline-per-chained-call": 0,
"object-curly-spacing": [0, "never"],
"object-shorthand": 0,
"one-var": 0,
"one-var-declaration-per-line": 0,
@@ -172,6 +172,7 @@ These rules are purely matters of style and are quite subjective.
* [new-cap](new-cap.md) - require a capital letter for constructors
* [new-parens](new-parens.md) - disallow the omission of parentheses when invoking a constructor with no arguments
* [newline-after-var](newline-after-var.md) - require or disallow an empty newline after variable declarations
* [newline-per-chained-call](newline-per-chained-call.md) - enforce newline after each call when chaining the calls
* [no-array-constructor](no-array-constructor.md) - disallow use of the `Array` constructor
* [no-bitwise](no-bitwise.md) - disallow use of bitwise operators
* [no-continue](no-continue.md) - disallow use of the `continue` statement
@@ -0,0 +1,126 @@
# Newline Per Chained Method Call (newline-per-chained-call)

Chained method calls on a single line without line breaks are harder to read. This rule enforces new line after each method call in the chain to make it more readable and easy to maintain.

Let's look at the following perfectly valid (but single line) code.

```js
d3.select('body').selectAll('p').data([4, 8, 15, 16, 23, 42 ]).enter().append('p').text(function(d) { return "I'm number " + d + "!"; });
```

However, with appropriate new lines, it becomes easy to read and understand. Look at the same code written below with line breaks after each call.

```js
d3
.select('body')
.selectAll('p')
.data([
4,
8,
15,
16,
23,
42
])
.enter()
.append('p')
.text(function (d) {
return "I'm number " + d + "!";
});
```

This rule reports such code and encourages new lines after each call in the chain as a good practice.

## Rule Details

This rule checks and reports the chained calls if there are no new lines after each call or deep member access.

### Options

The rule takes a single option `ignoreChainWithDepth`. The level/depth to be allowed is configurable through `ignoreChainWithDepth` option. This rule, in its default state, allows 2 levels.

* `ignoreChainWithDepth` Number of depths to be allowed (Default: `2`).

### Usage

Following patterns are considered problems with default configuration:

```js
/*eslint newline-per-chained-call: 2*/
_.chain({}).map(foo).filter(bar).value();
// Or
_.chain({}).map(foo).filter(bar);
// Or
_
.chain({}).map(foo)
.filter(bar);
// Or
obj.prop.method().prop
```

Following patterns are not considered problems with default configuration:

```js
/*eslint newline-per-chained-call: [2]*/
_
.chain({})
.map(foo)
.filter(bar)
.value();
// Or
_
.chain({})
.map(foo)
.filter(bar);
// Or
obj
.prop
.method()
.prop
```

Change the option `ignoreChainWithDepth` value to allow single line chains of that depth.

For example, when configuration is like this:

```js
{
"newline-per-chained-call": [2, {"ignoreChainWithDepth": 3}]
}
```

Following patterns are not considered problems:

```js
_.chain({}).map(foo);
// Or
obj.prop.method();
```

Following patterns are considered problems:

```js
_.chain({}).map(foo).filter(bar);
// Or
obj.prop.method().prop;
// Or
obj
.prop
.method().prop;
```

## When Not To Use It

If you have conflicting rules or when you are fine with chained calls on one line, you can safely turn this rule off.
@@ -0,0 +1,113 @@
/**
* @fileoverview Rule to ensure newline per method call when chaining calls
* @author Rajendra Patil
* @copyright 2016 Rajendra Patil. All rights reserved.
*/

"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = function(context) {

var options = context.options[0] || {},
codeStateMap = {},
ignoreChainWithDepth = options.ignoreChainWithDepth || 2;

/**
* Check and Capture State if the chained calls/memebers
* @param {ASTNode} node The node to check
* @param {object} codeState The state of the code current code to be filled
* @returns {void}
*/
function checkAndCaptureStateRecursively(node, codeState) {
var valid = false,
objectLineNumber,
propertyLineNumber;
if (node.callee) {
node = node.callee;
codeState.hasFunctionCall = true;
}

if (node.object) {
codeState.depth++;

objectLineNumber = node.object.loc.end.line;
propertyLineNumber = node.property.loc.end.line;
valid = propertyLineNumber > objectLineNumber;

if (!valid) {
codeState.reports.push({
node: node,
text: "Expected line break after `{{code}}`.",
depth: codeState.depth
});
}
// Recurse
checkAndCaptureStateRecursively(node.object, codeState);
}

}
/**
* Verify and report the captured state report
* @param {object} codeState contains the captured state with `hasFunctionCall, reports and depth`
* @returns {void}
*/
function reportState(codeState) {
var report;
if (codeState.hasFunctionCall && codeState.depth > ignoreChainWithDepth && codeState.reports) {
while (codeState.reports.length) {
report = codeState.reports.shift();
context.report(report.node, report.text, {
code: context.getSourceCode().getText(report.node.object).replace(/\r\n|\r|\n/g, "\\n") // Not to print newlines in error report
});
}
}
}

/**
* Initialize the node state object with default values.
* @returns {void}
*/
function initializeState() {
return {
visited: false,
hasFunctionCall: false,
depth: 1,
reports: []
};
}
/**
* Process the said node and recuse internally
* @param {ASTNode} node The node to check
* @returns {void}
*/
function processNode(node) {
var stateKey = [node.loc.start.line, node.loc.start.column].join("@"),
codeState = codeStateMap[stateKey] = (codeStateMap[stateKey] || initializeState());
if (!codeState.visited) {
codeState.visited = true;
checkAndCaptureStateRecursively(node, codeState);
}
reportState(codeState);
}

return {
"CallExpression": processNode,
"MemberExpression": processNode
};
};

module.exports.schema = [{
"type": "object",
"properties": {
"ignoreChainWithDepth": {
"type": "integer",
"minimum": 1,
"maximum": 10
}
},
"additionalProperties": false
}];
@@ -0,0 +1,114 @@
/**
* @fileoverview Tests for newline-per-chained-call rule.
* @author Rajendra Patil
* @copyright 2016 Rajendra Patil. All rights reserved.
*/

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

var rule = require("../../../lib/rules/newline-per-chained-call"),
RuleTester = require("../../../lib/testers/rule-tester");

var ruleTester = new RuleTester();
ruleTester.run("newline-per-chained-call", rule, {
valid: [{
code: "_\n.chain({})\n.map(foo)\n.filter(bar)\n.value();"
}, {
code: "a.b.c.d.e.f"
}, {
code: "a()\n.b()\n.c\n.e"
}, {
code: "var a = m1.m2(); var b = m1.m2();\nvar c = m1.m2()"
}, {
code: "var a = m1()\n.m2();"
}, {
code: "var a = m1();"
}, {
code: "var a = m1().m2.m3();",
options: [{
ignoreChainWithDepth: 3
}]
}, {
code: "var a = m1().m2.m3().m4.m5().m6.m7().m8;",
options: [{
ignoreChainWithDepth: 8
}]
}],
invalid: [{
code: "_\n.chain({}).map(foo).filter(bar).value();",
errors: [{
message: "Expected line break after `_\\n.chain({}).map(foo).filter(bar)`."
}, {
message: "Expected line break after `_\\n.chain({}).map(foo)`."
}, {
message: "Expected line break after `_\\n.chain({})`."
}]
}, {
code: "_\n.chain({})\n.map(foo)\n.filter(bar).value();",
errors: [{
message: "Expected line break after `_\\n.chain({})\\n.map(foo)\\n.filter(bar)`."
}]
}, {
code: "a()\n.b().c.e.d()",
errors: [{
message: "Expected line break after `a()\\n.b().c.e`."
}, {
message: "Expected line break after `a()\\n.b().c`."
}, {
message: "Expected line break after `a()\\n.b()`."
}]
}, {
code: "a().b().c.e.d()",
errors: [{
message: "Expected line break after `a().b().c.e`."
}, {
message: "Expected line break after `a().b().c`."
}, {
message: "Expected line break after `a().b()`."
}, {
message: "Expected line break after `a()`."
}]
}, {
code: "a.b.c.e.d()",
errors: [{
message: "Expected line break after `a.b.c.e`."
}, {
message: "Expected line break after `a.b.c`."
}, {
message: "Expected line break after `a.b`."
}, {
message: "Expected line break after `a`."
}]
}, {
code: "_.chain({}).map(a); ",
errors: [{
message: "Expected line break after `_.chain({})`."
}, {
message: "Expected line break after `_`."
}]
}, {
code: "var a = m1.m2();\n var b = m1.m2().m3().m4();",
errors: [{
message: "Expected line break after `m1.m2().m3()`."
}, {
message: "Expected line break after `m1.m2()`."
}, {
message: "Expected line break after `m1`."
}]
}, {
code: "var a = m1().m2\n.m3().m4();",
options: [{
ignoreChainWithDepth: 3
}],
errors: [{
message: "Expected line break after `m1().m2\\n.m3()`."
}, {
message: "Expected line break after `m1()`."
}]
}]

});

0 comments on commit b2aedfe

Please sign in to comment.
You can’t perform that action at this time.