Skip to content

Commit 8a05b7f

Browse files
committed
explicit-timer-delay: Add rule
1 parent af42ccb commit 8a05b7f

File tree

5 files changed

+443
-0
lines changed

5 files changed

+443
-0
lines changed

docs/rules/explicit-timer-delay.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Enforce or disallow explicit `delay` argument for `setTimeout()` and `setInterval()`
2+
3+
💼 This rule is enabled in the following [configs](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config): ✅ `recommended`, ☑️ `unopinionated`.
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
When using [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) or [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval), the `delay` parameter is optional and defaults to `0`. This rule allows you to enforce whether the `delay` argument should always be explicitly provided or omitted when it's `0`.
11+
12+
This provides flexibility for different team preferences:
13+
- `"always"` (default): Require explicit `delay` argument for clarity
14+
- `"never"`: Disallow explicit `0` delay (prefer implicit default)
15+
16+
## Examples
17+
18+
### With `"always"` (default)
19+
20+
```js
21+
//
22+
setTimeout(() => console.log('Hello'));
23+
setInterval(callback);
24+
window.setTimeout(() => console.log('Hello'));
25+
globalThis.setInterval(callback);
26+
27+
//
28+
setTimeout(() => console.log('Hello'), 0);
29+
setInterval(callback, 0);
30+
setTimeout(() => console.log('Hello'), 1000);
31+
window.setTimeout(() => console.log('Hello'), 0);
32+
globalThis.setInterval(callback, 100);
33+
```
34+
35+
### With `"never"`
36+
37+
```js
38+
//
39+
setTimeout(() => console.log('Hello'), 0);
40+
setInterval(callback, 0);
41+
window.setTimeout(() => console.log('Hello'), 0);
42+
globalThis.setInterval(callback, 0);
43+
44+
//
45+
setTimeout(() => console.log('Hello'));
46+
setInterval(callback);
47+
setTimeout(() => console.log('Hello'), 1000);
48+
window.setTimeout(() => console.log('Hello'));
49+
globalThis.setInterval(callback, 100);
50+
```
51+
52+
## Options
53+
54+
Type: `string`
55+
56+
Values: `"always"` (default) | `"never"`
57+
58+
### `"always"`
59+
60+
Requires an explicit `delay` argument for all `setTimeout()` and `setInterval()` calls.
61+
62+
```json
63+
{
64+
"unicorn/explicit-timer-delay": ["error", "always"]
65+
}
66+
```
67+
68+
### `"never"`
69+
70+
Disallows an explicit `delay` argument when it's `0`. Non-zero delays are still allowed.
71+
72+
```json
73+
{
74+
"unicorn/explicit-timer-delay": ["error", "never"]
75+
}
76+
```

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export default [
7171
| [escape-case](docs/rules/escape-case.md) | Require escape sequences to use uppercase or lowercase values. | ✅ ☑️ | 🔧 | |
7272
| [expiring-todo-comments](docs/rules/expiring-todo-comments.md) | Add expiration conditions to TODO comments. | ✅ ☑️ | | |
7373
| [explicit-length-check](docs/rules/explicit-length-check.md) | Enforce explicitly comparing the `length` or `size` property of a value. || 🔧 | 💡 |
74+
| [explicit-timer-delay](docs/rules/explicit-timer-delay.md) | Enforce or disallow explicit `delay` argument for `setTimeout()` and `setInterval()`. | ✅ ☑️ | 🔧 | |
7475
| [filename-case](docs/rules/filename-case.md) | Enforce a case style for filenames. || | |
7576
| [import-style](docs/rules/import-style.md) | Enforce specific import styles per module. | ✅ ☑️ | | |
7677
| [new-for-builtins](docs/rules/new-for-builtins.md) | Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. | ✅ ☑️ | 🔧 | 💡 |

rules/explicit-timer-delay.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import {isLiteral} from './ast/index.js';
2+
3+
const MODE_ALWAYS = 'always';
4+
const MODE_NEVER = 'never';
5+
6+
const MESSAGE_ID_MISSING_DELAY = 'missing-delay';
7+
const MESSAGE_ID_REDUNDANT_DELAY = 'redundant-delay';
8+
9+
const messages = {
10+
[MESSAGE_ID_MISSING_DELAY]: '`{{name}}` should have an explicit delay argument.',
11+
[MESSAGE_ID_REDUNDANT_DELAY]: '`{{name}}` should not have an explicit delay of `0`.',
12+
};
13+
14+
const timerFunctions = new Set(['setTimeout', 'setInterval']);
15+
16+
/**
17+
Check if a call expression is a timer function call.
18+
@param {import('estree').CallExpression} node - The call expression node.
19+
@param {import('eslint').SourceCode} sourceCode
20+
@returns {{isTimer: boolean, name?: string}} Object with isTimer flag and function name.
21+
*/
22+
const checkTimerCall = (node, sourceCode) => {
23+
const {callee} = node;
24+
25+
if (
26+
callee.type === 'Identifier'
27+
&& timerFunctions.has(callee.name)
28+
&& sourceCode.isGlobalReference(callee)
29+
) {
30+
return {isTimer: true, name: callee.name};
31+
}
32+
33+
if (
34+
callee.type === 'MemberExpression'
35+
&& !callee.computed
36+
&& callee.property.type === 'Identifier'
37+
&& timerFunctions.has(callee.property.name)
38+
) {
39+
const {object} = callee;
40+
41+
if (
42+
object.type === 'Identifier'
43+
&& sourceCode.isGlobalReference(object)
44+
) {
45+
return {isTimer: true, name: callee.property.name};
46+
}
47+
48+
return {isTimer: false};
49+
}
50+
51+
return {isTimer: false};
52+
};
53+
54+
/**
55+
Check if the delay argument is explicitly zero.
56+
@param {import('estree').Node} node - The argument node.
57+
@returns {boolean} True if the argument is zero.
58+
*/
59+
const isZeroDelay = node =>
60+
isLiteral(node, 0)
61+
|| (node.type === 'UnaryExpression' && node.operator === '-' && isLiteral(node.argument, 0));
62+
63+
/** @param {import('eslint').Rule.RuleContext} context */
64+
const create = context => {
65+
const mode = context.options[0] || MODE_ALWAYS;
66+
const {sourceCode} = context;
67+
68+
context.on('CallExpression', node => {
69+
if (node.optional) {
70+
return;
71+
}
72+
73+
const {isTimer, name} = checkTimerCall(node, sourceCode);
74+
75+
if (!isTimer) {
76+
return;
77+
}
78+
79+
const {arguments: arguments_} = node;
80+
const hasDelayArgument = arguments_.length >= 2;
81+
82+
if (mode === MODE_ALWAYS && !hasDelayArgument) {
83+
if (arguments_.length === 0) {
84+
return;
85+
}
86+
87+
const problem = {
88+
node,
89+
messageId: MESSAGE_ID_MISSING_DELAY,
90+
data: {name},
91+
};
92+
93+
const [firstArgument] = arguments_;
94+
if (firstArgument && firstArgument.type !== 'SpreadElement') {
95+
problem.fix = fixer => fixer.insertTextAfter(firstArgument, ', 0');
96+
}
97+
98+
return problem;
99+
}
100+
101+
if (mode === MODE_NEVER && hasDelayArgument) {
102+
const delayArgument = arguments_[1];
103+
104+
if (isZeroDelay(delayArgument)) {
105+
return {
106+
node: delayArgument,
107+
messageId: MESSAGE_ID_REDUNDANT_DELAY,
108+
data: {name},
109+
fix(fixer) {
110+
const firstArgument = arguments_[0];
111+
const [, firstArgumentEnd] = sourceCode.getRange(firstArgument);
112+
const [delayArgumentStart] = sourceCode.getRange(delayArgument);
113+
114+
return fixer.removeRange([firstArgumentEnd, delayArgumentStart + sourceCode.getText(delayArgument).length]);
115+
},
116+
};
117+
}
118+
}
119+
});
120+
};
121+
122+
const schema = [
123+
{
124+
enum: [MODE_ALWAYS, MODE_NEVER],
125+
},
126+
];
127+
128+
/** @type {import('eslint').Rule.RuleModule} */
129+
const config = {
130+
create,
131+
meta: {
132+
type: 'suggestion',
133+
docs: {
134+
description: 'Enforce or disallow explicit `delay` argument for `setTimeout()` and `setInterval()`.',
135+
recommended: 'unopinionated',
136+
},
137+
fixable: 'code',
138+
schema,
139+
defaultOptions: [MODE_ALWAYS],
140+
messages,
141+
},
142+
};
143+
144+
export default config;

rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {default as 'error-message'} from './error-message.js';
1414
export {default as 'escape-case'} from './escape-case.js';
1515
export {default as 'expiring-todo-comments'} from './expiring-todo-comments.js';
1616
export {default as 'explicit-length-check'} from './explicit-length-check.js';
17+
export {default as 'explicit-timer-delay'} from './explicit-timer-delay.js';
1718
export {default as 'filename-case'} from './filename-case.js';
1819
export {default as 'import-style'} from './import-style.js';
1920
export {default as 'new-for-builtins'} from './new-for-builtins.js';

0 commit comments

Comments
 (0)