Skip to content

Commit 907753f

Browse files
authored
feat(no-callback-in-promise): add timeoutsErr option (#514)
Also: - fix(`no-callback-in-promise`): ensure timeouts do not err (by default)
1 parent 2cf1732 commit 907753f

File tree

3 files changed

+145
-21
lines changed

3 files changed

+145
-21
lines changed

__tests__/no-callback-in-promise.js

+96-7
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ ruleTester.run('no-callback-in-promise', rule, {
1717
'function thing(callback) { callback() }',
1818
'doSomething(function(err) { callback(err) })',
1919

20-
// TODO: support safe callbacks (see #220)
21-
// 'whatever.then((err) => { process.nextTick(() => cb()) })',
22-
// 'whatever.then((err) => { setImmediate(() => cb())) })',
23-
// 'whatever.then((err) => setImmediate(() => cb()))',
24-
// 'whatever.then((err) => process.nextTick(() => cb()))',
25-
// 'whatever.then((err) => process.nextTick(cb))',
26-
// 'whatever.then((err) => setImmediate(cb))',
20+
// Support safe callbacks (#220)
21+
'whatever.then((err) => { process.nextTick(() => cb()) })',
22+
'whatever.then((err) => { setImmediate(() => cb()) })',
23+
'whatever.then((err) => setImmediate(() => cb()))',
24+
'whatever.then((err) => process.nextTick(() => cb()))',
25+
'whatever.then((err) => process.nextTick(cb))',
26+
'whatever.then((err) => setImmediate(cb))',
2727

2828
// arrow functions and other things
2929
'let thing = (cb) => cb()',
@@ -97,5 +97,94 @@ ruleTester.run('no-callback-in-promise', rule, {
9797
code: 'a.catch(function(err) { callback(err) })',
9898
errors: [{ message: errorMessage }],
9999
},
100+
101+
// #167
102+
{
103+
code: `
104+
function wait (callback) {
105+
return Promise.resolve()
106+
.then(() => {
107+
setTimeout(callback);
108+
});
109+
}
110+
`,
111+
errors: [{ message: errorMessage }],
112+
options: [
113+
{
114+
timeoutsErr: true,
115+
},
116+
],
117+
},
118+
{
119+
code: `
120+
function wait (callback) {
121+
return Promise.resolve()
122+
.then(() => {
123+
setTimeout(() => callback());
124+
});
125+
}
126+
`,
127+
errors: [{ message: errorMessage }],
128+
options: [
129+
{
130+
timeoutsErr: true,
131+
},
132+
],
133+
},
134+
135+
{
136+
code: 'whatever.then((err) => { process.nextTick(() => cb()) })',
137+
errors: [{ message: errorMessage }],
138+
options: [
139+
{
140+
timeoutsErr: true,
141+
},
142+
],
143+
},
144+
{
145+
code: 'whatever.then((err) => { setImmediate(() => cb()) })',
146+
errors: [{ message: errorMessage }],
147+
options: [
148+
{
149+
timeoutsErr: true,
150+
},
151+
],
152+
},
153+
{
154+
code: 'whatever.then((err) => setImmediate(() => cb()))',
155+
errors: [{ message: errorMessage }],
156+
options: [
157+
{
158+
timeoutsErr: true,
159+
},
160+
],
161+
},
162+
{
163+
code: 'whatever.then((err) => process.nextTick(() => cb()))',
164+
errors: [{ message: errorMessage }],
165+
options: [
166+
{
167+
timeoutsErr: true,
168+
},
169+
],
170+
},
171+
{
172+
code: 'whatever.then((err) => process.nextTick(cb))',
173+
errors: [{ message: errorMessage }],
174+
options: [
175+
{
176+
timeoutsErr: true,
177+
},
178+
],
179+
},
180+
{
181+
code: 'whatever.then((err) => setImmediate(cb))',
182+
errors: [{ message: errorMessage }],
183+
options: [
184+
{
185+
timeoutsErr: true,
186+
},
187+
],
188+
},
100189
],
101190
})

docs/rules/no-callback-in-promise.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,14 @@ Callback got called with: null data
3131
Callback got called with: My error null
3232
```
3333

34-
**How to fix it?**
34+
## Options
35+
36+
### `timeoutsErr`
37+
38+
Boolean as to whether callbacks in timeout functions like `setTimeout` will err.
39+
Defaults to `false`.
40+
41+
## How to fix it?
3542

3643
Ensure that your callback invocations are wrapped by a deferred execution
3744
function such as:

rules/no-callback-in-promise.js

+41-13
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,28 @@
77

88
const { getAncestors } = require('./lib/eslint-compat')
99
const getDocsUrl = require('./lib/get-docs-url')
10-
const hasPromiseCallback = require('./lib/has-promise-callback')
1110
const isInsidePromise = require('./lib/is-inside-promise')
1211
const isCallback = require('./lib/is-callback')
1312

1413
const CB_BLACKLIST = ['callback', 'cb', 'next', 'done']
14+
const TIMEOUT_WHITELIST = [
15+
'setImmediate',
16+
'setTimeout',
17+
'requestAnimationFrame',
18+
'nextTick',
19+
]
20+
21+
const isInsideTimeout = (node) => {
22+
const isFunctionExpression =
23+
node.type === 'FunctionExpression' ||
24+
node.type === 'ArrowFunctionExpression'
25+
const parent = node.parent || {}
26+
const callee = parent.callee || {}
27+
const name = (callee.property && callee.property.name) || callee.name || ''
28+
const parentIsTimeout = TIMEOUT_WHITELIST.includes(name)
29+
const isInCB = isFunctionExpression && parentIsTimeout
30+
return isInCB
31+
}
1532

1633
module.exports = {
1734
meta: {
@@ -34,32 +51,43 @@ module.exports = {
3451
type: 'string',
3552
},
3653
},
54+
timeoutsErr: {
55+
type: 'boolean',
56+
},
3757
},
3858
additionalProperties: false,
3959
},
4060
],
4161
},
4262
create(context) {
63+
const { timeoutsErr = false } = context.options[0] || {}
64+
4365
return {
4466
CallExpression(node) {
4567
const options = context.options[0] || {}
4668
const exceptions = options.exceptions || []
4769
if (!isCallback(node, exceptions)) {
48-
// in general we send you packing if you're not a callback
49-
// but we also need to watch out for whatever.then(cb)
50-
if (hasPromiseCallback(node)) {
51-
const name =
52-
node.arguments && node.arguments[0] && node.arguments[0].name
53-
if (!exceptions.includes(name) && CB_BLACKLIST.includes(name)) {
54-
context.report({
55-
node: node.arguments[0],
56-
messageId: 'callback',
57-
})
58-
}
70+
const callingName = node.callee.name || node.callee.property?.name
71+
const name =
72+
node.arguments && node.arguments[0] && node.arguments[0].name
73+
if (
74+
!exceptions.includes(name) &&
75+
CB_BLACKLIST.includes(name) &&
76+
(timeoutsErr || !TIMEOUT_WHITELIST.includes(callingName))
77+
) {
78+
context.report({
79+
node: node.arguments[0],
80+
messageId: 'callback',
81+
})
5982
}
6083
return
6184
}
62-
if (getAncestors(context, node).some(isInsidePromise)) {
85+
86+
const ancestors = getAncestors(context, node)
87+
if (
88+
ancestors.some(isInsidePromise) &&
89+
(timeoutsErr || !ancestors.some(isInsideTimeout))
90+
) {
6391
context.report({
6492
node,
6593
messageId: 'callback',

0 commit comments

Comments
 (0)