Skip to content

Commit e47ec51

Browse files
committed
feat(rules): add 'array-callback-return' rule
1 parent cc8a3ce commit e47ec51

File tree

6 files changed

+384
-1
lines changed

6 files changed

+384
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Rule | Default | Options
4848
[no-shadowing][] | 1 |
4949
[use-first-last][] | 1 |
5050
[no-get-in-it][] | 1 |
51+
[array-callback-return][] | 1 |
5152
[by-css-shortcut][] | 0 |
5253

5354
For example, the `missing-perform` rule is enabled by default and will cause
@@ -77,6 +78,7 @@ See [configuring rules][] for more information.
7778
[no-shadowing]: docs/rules/no-shadowing.md
7879
[use-first-last]: docs/rules/use-first-last.md
7980
[no-get-in-it]: docs/rules/no-get-in-it.md
81+
[array-callback-return]: docs/rules/array-callback-return.md
8082
[by-css-shortcut]: docs/rules/by-css-shortcut.md
8183
[configuring rules]: http://eslint.org/docs/user-guide/configuring#configuring-rules
8284

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Enforce `return` statements in callbacks of `ElementArrayFinder` methods
2+
3+
`ElementArrayFinder` has multiple methods for filtering, mapping, reducing:
4+
5+
* [`map()`](http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.map)
6+
* [`filter()`](http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.filter)
7+
* [`reduce()`](http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.reduce)
8+
9+
The callbacks for these methods have to return something for the method to work properly.
10+
11+
This rule would issue a warning if there is no `return` statement detected in the callback.
12+
13+
## Known Limitations
14+
15+
This rule checks callback functions of methods with the given names only if called on `element.all()` or `$$()` explicitly.
16+
This means, that if there is, for example, a page object field which is later filtered:
17+
18+
```js
19+
myPage.rows.filter(function (row) {
20+
row.getText();
21+
});
22+
```
23+
24+
it would not be detected as a violation. Look into having [`array-callback-return`](http://eslint.org/docs/rules/array-callback-return) built-in ESLint rule enabled to catch these cases.
25+
26+
## Rule details
27+
28+
Any use of the following patterns are considered warnings:
29+
30+
```js
31+
element.all(by.css(".myclass")).filter(function() {
32+
elm.getText().then(function (text) {
33+
return text.indexOf("test") >= 0;
34+
})
35+
});
36+
37+
$$(".myclass").filter(function cb() { if (a) return true; });
38+
element(by.id("myid")).$$(".myclass").filter(function() { switch (a) { case 0: break; default: return true; } });
39+
$$(".myclass").filter(function() { return; });
40+
$$(".myclass").filter(function() { if (a) return; else return; });
41+
$$(".myclass").filter(a ? function() {} : function() {});
42+
$$(".myclass").filter(function(){ return function() {}; }())
43+
```
44+
45+
The following patterns are not warnings:
46+
47+
```js
48+
element.all(by.css(".myclass")).filter(function(elm) {
49+
return elm.getText().then(function (text) {
50+
return text.indexOf("test") >= 0;
51+
})
52+
});
53+
54+
$$(".myclass").reduce(function() { return true; });
55+
$$(".myclass").reduce(function() { switch (a) { case 0: bar(); default: return true; } })
56+
var elements = element.all(by.css(".myclass"));
57+
```

index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var useSimpleRepeaters = require('./lib/rules/use-simple-repeaters')
1313
var noShadowing = require('./lib/rules/no-shadowing')
1414
var useFirstLast = require('./lib/rules/use-first-last')
1515
var noGetInIt = require('./lib/rules/no-get-in-it')
16+
var arrayCallbackReturn = require('./lib/rules/array-callback-return')
1617

1718
module.exports = {
1819
rules: {
@@ -28,7 +29,8 @@ module.exports = {
2829
'use-simple-repeaters': useSimpleRepeaters,
2930
'no-shadowing': noShadowing,
3031
'use-first-last': useFirstLast,
31-
'no-get-in-it': noGetInIt
32+
'no-get-in-it': noGetInIt,
33+
'array-callback-return': arrayCallbackReturn
3234
},
3335
configs: {
3436
recommended: {
@@ -45,6 +47,7 @@ module.exports = {
4547
'protractor/no-shadowing': 1,
4648
'protractor/use-first-last': 1,
4749
'protractor/no-get-in-it': 1,
50+
'protractor/array-callback-return': 1,
4851
'protractor/by-css-shortcut': 0
4952
},
5053
globals: {

lib/is-element-array-finder.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict'
2+
3+
/**
4+
* Checks a given MemberExpression node is an ElementArrayFinder instance.
5+
*
6+
* @fileoverview Utility function to determine if a node is an ElementArrayFinder
7+
* @author Alexander Afanasyev
8+
*/
9+
module.exports = function (node) {
10+
// handling $$ shortcut
11+
var object = node.object
12+
if (object && object.callee && object.callee.name === '$$') {
13+
return true
14+
}
15+
16+
if (object) {
17+
var callee = object.callee
18+
if (callee && callee.object && callee.property) {
19+
// handling chained $$
20+
if (callee.property.name === '$$') {
21+
return true
22+
}
23+
24+
// handling element.all() and chained element() and all()
25+
if (callee.property.name === 'all' &&
26+
(callee.object.name === 'element' || (callee.object.callee && callee.object.callee.name === 'element'))) {
27+
return true
28+
}
29+
}
30+
}
31+
32+
return false
33+
}

lib/rules/array-callback-return.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
'use strict'
2+
3+
/**
4+
* @fileoverview Rule to enforce return statements in callbacks of ElementArrayFinder's methods
5+
* @author Alexander Afanasyev (based on Toru Nagashima's work)
6+
*/
7+
8+
var astUtils = require('eslint/lib/ast-utils')
9+
var isElementArrayFinder = require('../is-element-array-finder')
10+
11+
var TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/
12+
var TARGET_METHODS = /^(?:filter|map|reduce)$/
13+
14+
/**
15+
* Checks a given code path segment is reachable.
16+
*
17+
* @param {CodePathSegment} segment - A segment to check.
18+
* @returns {boolean} `true` if the segment is reachable.
19+
*/
20+
function isReachable (segment) {
21+
return segment.reachable
22+
}
23+
24+
/**
25+
* Gets a readable location.
26+
*
27+
* - FunctionExpression -> the function name or `function` keyword.
28+
* - ArrowFunctionExpression -> `=>` token.
29+
*
30+
* @param {ASTNode} node - A function node to get.
31+
* @param {SourceCode} sourceCode - A source code to get tokens.
32+
* @returns {ASTNode|Token} The node or the token of a location.
33+
*/
34+
function getLocation (node, sourceCode) {
35+
if (node.type === 'ArrowFunctionExpression') {
36+
return sourceCode.getTokenBefore(node.body)
37+
}
38+
return node.id || node
39+
}
40+
41+
/**
42+
* Checks a given node is a MemberExpression node which has the specified name's
43+
* property.
44+
*
45+
* @param {ASTNode} node - A node to check.
46+
* @returns {boolean} `true` if the node is a MemberExpression node which has
47+
* the specified name's property
48+
*/
49+
function isTargetMethod (node) {
50+
return (isElementArrayFinder(node) &&
51+
node.type === 'MemberExpression' &&
52+
node.property &&
53+
TARGET_METHODS.test(node.property.name)
54+
)
55+
}
56+
57+
/**
58+
* Checks whether or not a given node is a function expression which is the
59+
* callback of an array method.
60+
*
61+
* @param {ASTNode} node - A node to check. This is one of
62+
* FunctionExpression or ArrowFunctionExpression.
63+
* @returns {boolean} `true` if the node is the callback of an array method.
64+
*/
65+
function isCallbackOfArrayMethod (node) {
66+
while (node) {
67+
var parent = node.parent
68+
69+
switch (parent.type) {
70+
case 'LogicalExpression':
71+
case 'ConditionalExpression':
72+
node = parent
73+
break
74+
75+
case 'ReturnStatement':
76+
var func = astUtils.getUpperFunction(parent)
77+
78+
if (func === null || !astUtils.isCallee(func)) {
79+
return false
80+
}
81+
node = func.parent
82+
break
83+
84+
case 'CallExpression':
85+
if (isTargetMethod(parent.callee)) {
86+
return (parent.arguments.length >= 1 && parent.arguments[0] === node)
87+
}
88+
return false
89+
90+
// Otherwise this node is not target.
91+
/* istanbul ignore next: unreachable */
92+
default:
93+
return false
94+
}
95+
}
96+
97+
/* istanbul ignore next: unreachable */
98+
return false
99+
}
100+
101+
module.exports = {
102+
meta: {
103+
schema: []
104+
},
105+
106+
create: function (context) {
107+
var funcInfo = {
108+
upper: null,
109+
codePath: null,
110+
hasReturn: false,
111+
shouldCheck: false
112+
}
113+
114+
/**
115+
* Checks whether or not the last code path segment is reachable.
116+
* Then reports this function if the segment is reachable.
117+
*
118+
* If the last code path segment is reachable, there are paths which are not
119+
* returned or thrown.
120+
*
121+
* @param {ASTNode} node - A node to check.
122+
* @returns {void}
123+
*/
124+
function checkLastSegment (node) {
125+
if (funcInfo.shouldCheck &&
126+
funcInfo.codePath.currentSegments.some(isReachable)
127+
) {
128+
context.report({
129+
node: node,
130+
loc: getLocation(node, context.getSourceCode()).loc.start,
131+
message: funcInfo.hasReturn
132+
? 'Expected to return a value at the end of this function'
133+
: 'Expected to return a value in this function'
134+
})
135+
}
136+
}
137+
138+
return {
139+
// Stacks this function's information.
140+
'onCodePathStart': function (codePath, node) {
141+
funcInfo = {
142+
upper: funcInfo,
143+
codePath: codePath,
144+
hasReturn: false,
145+
shouldCheck: TARGET_NODE_TYPE.test(node.type) &&
146+
node.body.type === 'BlockStatement' &&
147+
isCallbackOfArrayMethod(node)
148+
}
149+
},
150+
151+
// Pops this function's information.
152+
'onCodePathEnd': function () {
153+
funcInfo = funcInfo.upper
154+
},
155+
156+
// Checks the return statement is valid.
157+
'ReturnStatement': function (node) {
158+
if (funcInfo.shouldCheck) {
159+
funcInfo.hasReturn = true
160+
161+
if (!node.argument) {
162+
context.report({
163+
node: node,
164+
message: 'Expected a return value'
165+
})
166+
}
167+
}
168+
},
169+
170+
// Reports a given function if the last path is reachable.
171+
'FunctionExpression:exit': checkLastSegment,
172+
'ArrowFunctionExpression:exit': checkLastSegment
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)