Skip to content

Commit 28caa17

Browse files
committed
add code path analysis to always-return
This will also change the reported location of errors to the inner function that failed to return, rather than referring to it's enclosing Promise I bumped up the the version number (2.0.1 -> 3.0.0) because the changed location reporting could un-suppress linter errors that have been suppressed with `// eslint-disable-line promise/always-return` fixes #8 fixes #18 fixes #24
1 parent e21b820 commit 28caa17

File tree

4 files changed

+105
-22
lines changed

4 files changed

+105
-22
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,15 @@ We also allow someone to `throw` inside a `then()` which is essentially the same
6161
myPromise.then((val) => val * 2));
6262
myPromise.then(function(val) { return val * 2; });
6363
myPromise.then(doSomething); // could be either
64+
myPromise.then((b) => { if (b) { return "yes" } else { return "no" } });
6465
```
6566

6667
#### Invalid
6768

6869
```js
6970
myPromise.then(function(val) {});
7071
myPromise.then(() => { doSomething(); });
72+
myPromise.then((b) => { if (b) { return "yes" } else { forgotToReturn(); } });
7173
```
7274

7375
### `param-names`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-promise",
3-
"version": "2.0.1",
3+
"version": "3.0.0",
44
"description": "Enforce best practices for JavaScript promises",
55
"keywords": [
66
"eslint",

rules/always-return.js

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,88 @@ function isFunctionWithBlockStatement (node) {
88
return false
99
}
1010

11-
function isReturnOrThrowStatement (node) {
12-
return node.type === 'ReturnStatement' || node.type === 'ThrowStatement'
11+
function isThenExpression (node) {
12+
return (
13+
node.type === 'CallExpression' &&
14+
node.callee.type === 'MemberExpression' &&
15+
node.callee.property.name === 'then'
16+
)
17+
}
18+
19+
function isInlineThenFunctionExpression (node) {
20+
return (
21+
isFunctionWithBlockStatement(node) &&
22+
isThenExpression(node.parent)
23+
)
24+
}
25+
26+
function includes (arr, val) {
27+
return arr.indexOf(val) !== -1
28+
}
29+
30+
function last (arr) {
31+
return arr[arr.length - 1]
1332
}
1433

1534
module.exports = {
1635
create: function (context) {
17-
return {
18-
MemberExpression: function (node) {
19-
var firstArg, body, lastStatement
20-
21-
if (node.property.name !== 'then' || node.parent.type !== 'CallExpression') {
22-
return
23-
}
24-
25-
firstArg = node.parent.arguments[0]
26-
if (!firstArg || !isFunctionWithBlockStatement(firstArg)) {
27-
return
28-
}
29-
30-
body = firstArg.body.body
31-
lastStatement = body[body.length - 1]
32-
if (!lastStatement || !isReturnOrThrowStatement(lastStatement)) {
33-
context.report(node, 'Each then() should return a value or throw')
34-
}
36+
var funcInfoStack = []
37+
var CPSIDStack = []
38+
39+
function isEveryBranchReturning (funcInfo) {
40+
// We need to check noCurrentCPSIsOnTheCPSStack because of what
41+
// seems like a bug in eslint where 'FunctionExpression:exit' events occur
42+
// before all of their constituent codePathSegments have fired their
43+
// 'onCodePathSegmentEnd' events
44+
var currentIDs = funcInfo.codePath.currentSegments.map(x => x.id)
45+
var noCurrentCPSIsOnTheCPSStack = !currentIDs.some((id) => includes(CPSIDStack, id))
46+
47+
var finalIDs = funcInfo.codePath.finalSegments.map(x => x.id)
48+
var everyFinalCPSIsReturning = finalIDs.every((id) => includes(funcInfo.explicitlyReturningCPSIDs, id))
49+
50+
return noCurrentCPSIsOnTheCPSStack && everyFinalCPSIsReturning
51+
}
52+
53+
function onFunctionExpressionExit (node) {
54+
if (!isInlineThenFunctionExpression(node)) {
55+
return
56+
}
57+
58+
var funcInfo = last(funcInfoStack)
59+
if (!isEveryBranchReturning(funcInfo)) {
60+
context.report(node, 'Each then() should return a value or throw')
3561
}
3662
}
63+
64+
function markCurrentCodePathSegmentAsReturning () {
65+
var funcInfo = last(funcInfoStack)
66+
var currentCPSID = last(CPSIDStack)
67+
funcInfo.explicitlyReturningCPSIDs.push(currentCPSID)
68+
}
69+
70+
return {
71+
onCodePathStart: function (codePath, node) {
72+
funcInfoStack.push({
73+
codePath: codePath,
74+
explicitlyReturningCPSIDs: []
75+
})
76+
},
77+
78+
onCodePathEnd: function (codePath, node) {
79+
funcInfoStack.pop()
80+
},
81+
82+
onCodePathSegmentEnd: function (segment, node) {
83+
CPSIDStack.pop()
84+
},
85+
onCodePathSegmentStart: function (segment, node) {
86+
CPSIDStack.push(segment.id)
87+
},
88+
89+
ReturnStatement: markCurrentCodePathSegmentAsReturning,
90+
ThrowStatement: markCurrentCodePathSegmentAsReturning,
91+
'FunctionExpression:exit': onFunctionExpressionExit,
92+
'ArrowFunctionExpression:exit': onFunctionExpressionExit
93+
}
3794
}
3895
}

test/always-return.test.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ruleTester.run('always-return', rule, {
99
valid: [
1010
{ code: 'hey.then(x => x)', parserOptions: parserOptions },
1111
{ code: 'hey.then(x => ({}))', parserOptions: parserOptions },
12+
{ code: 'hey.then(x => { return; })', parserOptions: parserOptions },
1213
{ code: 'hey.then(x => { return x * 10 })', parserOptions: parserOptions },
1314
{ code: 'hey.then(function() { return 42; })', parserOptions: parserOptions },
1415
{ code: 'hey.then(function() { return new Promise(); })', parserOptions: parserOptions },
@@ -19,7 +20,10 @@ ruleTester.run('always-return', rule, {
1920
{ code: 'hey.then(function(x) { if (x) { return x; } throw new Error("no x"); })', parserOptions: parserOptions },
2021
{ code: 'hey.then(x => { throw new Error("msg"); })', parserOptions: parserOptions },
2122
{ code: 'hey.then(x => { if (!x) { throw new Error("no x"); } return x; })', parserOptions: parserOptions },
22-
{ code: 'hey.then(x => { if (x) { return x; } throw new Error("no x"); })', parserOptions: parserOptions }
23+
{ code: 'hey.then(x => { if (x) { return x; } throw new Error("no x"); })', parserOptions: parserOptions },
24+
{ code: 'hey.then(x => { var f = function() { }; return f; })', parserOptions: parserOptions },
25+
{ code: 'hey.then(x => { if (x) { return x; } else { return x; } })', parserOptions: parserOptions },
26+
{ code: 'hey.then(x => { return x; var y = "unreachable"; })', parserOptions: parserOptions }
2327
],
2428

2529
invalid: [
@@ -40,9 +44,29 @@ ruleTester.run('always-return', rule, {
4044
code: 'hey.then(function() { }).then(function() { })',
4145
errors: [ { message: message }, { message: message } ]
4246
},
47+
{
48+
code: 'hey.then(function() { return; }).then(function() { })',
49+
errors: [ { message: message } ]
50+
},
4351
{
4452
code: 'hey.then(function() { doSomethingWicked(); })',
4553
errors: [ { message: message } ]
54+
},
55+
{
56+
code: 'hey.then(function() { if (x) { return x; } })',
57+
errors: [ { message: message } ]
58+
},
59+
{
60+
code: 'hey.then(function() { if (x) { return x; } else { }})',
61+
errors: [ { message: message } ]
62+
},
63+
{
64+
code: 'hey.then(function() { if (x) { } else { return x; }})',
65+
errors: [ { message: message } ]
66+
},
67+
{
68+
code: 'hey.then(function() { if (x) { return you.then(function() { return x; }); } })',
69+
errors: [ { message: message } ]
4670
}
4771
]
4872
})

0 commit comments

Comments
 (0)