Skip to content

Commit 08052e8

Browse files
authored
fix(no-nesting): nested references vars in closure (#361)
1 parent ac98c63 commit 08052e8

File tree

3 files changed

+140
-2
lines changed

3 files changed

+140
-2
lines changed

__tests__/no-nesting.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ ruleTester.run('no-nesting', rule, {
4545
'doThing().then(function() { return Promise.resolve(4) })',
4646
'doThing().then(() => Promise.resolve(4))',
4747
'doThing().then(() => Promise.all([a]))',
48+
49+
// references vars in closure
50+
`doThing()
51+
.then(a => getB(a)
52+
.then(b => getC(a, b))
53+
)`,
54+
`doThing()
55+
.then(a => {
56+
const c = a * 2;
57+
return getB(c).then(b => getC(c, b))
58+
})`,
4859
],
4960

5061
invalid: [
@@ -80,5 +91,24 @@ ruleTester.run('no-nesting', rule, {
8091
code: 'doThing().then(() => b.catch())',
8192
errors: [{ message: errorMessage }],
8293
},
94+
// references vars in closure
95+
{
96+
code: `
97+
doThing()
98+
.then(a => getB(a)
99+
.then(b => getC(b))
100+
)`,
101+
errors: [{ message: errorMessage, line: 4 }],
102+
},
103+
{
104+
code: `
105+
doThing()
106+
.then(a => getB(a)
107+
.then(b => getC(a, b)
108+
.then(c => getD(a, c))
109+
)
110+
)`,
111+
errors: [{ message: errorMessage, line: 5 }],
112+
},
83113
],
84114
})

rules/lib/has-promise-callback.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@
66

77
'use strict'
88

9+
/**
10+
* @typedef {import('estree').SimpleCallExpression} CallExpression
11+
* @typedef {import('estree').MemberExpression} MemberExpression
12+
* @typedef {import('estree').Identifier} Identifier
13+
*
14+
* @typedef {object} NameIsThenOrCatch
15+
* @property {'then' | 'catch'} name
16+
*
17+
* @typedef {object} PropertyIsThenOrCatch
18+
* @property {Identifier & NameIsThenOrCatch} property
19+
*
20+
* @typedef {object} CalleeIsPromiseCallback
21+
* @property {MemberExpression & PropertyIsThenOrCatch} callee
22+
*
23+
* @typedef {CallExpression & CalleeIsPromiseCallback} HasPromiseCallback
24+
*/
25+
/**
26+
* @param {import('estree').Node} node
27+
* @returns {node is HasPromiseCallback}
28+
*/
929
function hasPromiseCallback(node) {
1030
// istanbul ignore if -- only being called within `CallExpression`
1131
if (node.type !== 'CallExpression') return

rules/no-nesting.js

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,100 @@ module.exports = {
1818
schema: [],
1919
},
2020
create(context) {
21+
/**
22+
* Array of callback function scopes.
23+
* Scopes are in order closest to the current node.
24+
* @type {import('eslint').Scope.Scope[]}
25+
*/
26+
const callbackScopes = []
27+
28+
/**
29+
* @param {import('eslint').Scope.Scope} scope
30+
* @returns {Iterable<import('eslint').Scope.Reference>}
31+
*/
32+
function* iterateDefinedReferences(scope) {
33+
for (const variable of scope.variables) {
34+
for (const reference of variable.references) {
35+
yield reference
36+
}
37+
}
38+
}
39+
2140
return {
41+
':function'(node) {
42+
if (isInsidePromise(node)) {
43+
callbackScopes.unshift(context.getScope())
44+
}
45+
},
46+
':function:exit'(node) {
47+
if (isInsidePromise(node)) {
48+
callbackScopes.shift()
49+
}
50+
},
2251
CallExpression(node) {
2352
if (!hasPromiseCallback(node)) return
24-
if (context.getAncestors().some(isInsidePromise)) {
25-
context.report({ node, message: 'Avoid nesting promises.' })
53+
if (!callbackScopes.length) {
54+
// The node is not in the callback function.
55+
return
2656
}
57+
58+
// Checks if the argument callback uses variables defined in the closest callback function scope.
59+
//
60+
// e.g.
61+
// ```
62+
// doThing()
63+
// .then(a => getB(a)
64+
// .then(b => getC(a, b))
65+
// )
66+
// ```
67+
//
68+
// In the above case, Since the variables it references are undef,
69+
// we cannot refactor the nesting like following:
70+
// ```
71+
// doThing()
72+
// .then(a => getB(a))
73+
// .then(b => getC(a, b))
74+
// ```
75+
//
76+
// However, `getD` can be refactored in the following:
77+
// ```
78+
// doThing()
79+
// .then(a => getB(a)
80+
// .then(b => getC(a, b)
81+
// .then(c => getD(a, c))
82+
// )
83+
// )
84+
// ```
85+
// ↓
86+
// ```
87+
// doThing()
88+
// .then(a => getB(a)
89+
// .then(b => getC(a, b))
90+
// .then(c => getD(a, c))
91+
// )
92+
// ```
93+
// This is why we only check the closest callback function scope.
94+
//
95+
const closestCallbackScope = callbackScopes[0]
96+
for (const reference of iterateDefinedReferences(
97+
closestCallbackScope
98+
)) {
99+
if (
100+
node.arguments.some(
101+
(arg) =>
102+
arg.range[0] <= reference.identifier.range[0] &&
103+
reference.identifier.range[1] <= arg.range[1]
104+
)
105+
) {
106+
// Argument callbacks refer to variables defined in the callback function.
107+
return
108+
}
109+
}
110+
111+
context.report({
112+
node: node.callee.property,
113+
message: 'Avoid nesting promises.',
114+
})
27115
},
28116
}
29117
},

0 commit comments

Comments
 (0)