1 | /**
|
2 | * Rule: no-nesting
|
3 | * Avoid nesting your promises.
|
4 | */
|
5 |
|
6 |
|
7 |
|
8 | const getDocsUrl = require('./lib/get-docs-url')
|
9 | const hasPromiseCallback = require('./lib/has-promise-callback')
|
10 | const isInsidePromise = require('./lib/is-inside-promise')
|
11 |
|
12 | module.exports = {
|
13 | meta: {
|
14 | type: 'suggestion',
|
15 | docs: {
|
16 | url: getDocsUrl('no-nesting'),
|
17 | },
|
18 | schema: [],
|
19 | },
|
20 | 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 |
|
40 | 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 | },
|
51 | CallExpression(node) {
|
52 | if (!hasPromiseCallback(node)) return
|
53 | if (!callbackScopes.length) {
|
54 | // The node is not in the callback function.
|
55 | return
|
56 | }
|
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 | })
|
115 | },
|
116 | }
|
117 | },
|
118 | }
|