UNPKG

eslint-plugin-unicorn

Version:
157 lines (132 loc) 3.88 kB
import {isLiteral} from './ast/index.js'; import {getParenthesizedRange} from './utils/index.js'; const MODE_ALWAYS = 'always'; const MODE_NEVER = 'never'; const MESSAGE_ID_MISSING_DELAY = 'missing-delay'; const MESSAGE_ID_REDUNDANT_DELAY = 'redundant-delay'; const messages = { [MESSAGE_ID_MISSING_DELAY]: '`{{name}}` should have an explicit delay argument.', [MESSAGE_ID_REDUNDANT_DELAY]: '`{{name}}` should not have an explicit delay of `0`.', }; const timerFunctions = new Set(['setTimeout', 'setInterval']); const globalObjects = new Set(['window', 'globalThis', 'global', 'self']); /** Check if a call expression is a timer function call. @param {import('estree').CallExpression} node - The call expression node. @param {import('eslint').SourceCode} sourceCode @returns {{isTimer: boolean, name?: string}} Object with isTimer flag and function name. */ const checkTimerCall = (node, sourceCode) => { const {callee} = node; if ( callee.type === 'Identifier' && timerFunctions.has(callee.name) && sourceCode.isGlobalReference(callee) ) { return {isTimer: true, name: callee.name}; } if ( callee.type === 'MemberExpression' && !callee.computed && callee.property.type === 'Identifier' && timerFunctions.has(callee.property.name) ) { const {object} = callee; if ( object.type === 'Identifier' && globalObjects.has(object.name) && sourceCode.isGlobalReference(object) ) { return {isTimer: true, name: callee.property.name}; } return {isTimer: false}; } return {isTimer: false}; }; /** Check if the delay argument is explicitly zero. @param {import('estree').Node} node - The argument node. @returns {boolean} True if the argument is zero. */ const isZeroDelay = node => isLiteral(node, 0) || ( node.type === 'UnaryExpression' && (node.operator === '+' || node.operator === '-') && isZeroDelay(node.argument) ); /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const mode = context.options[0]; const {sourceCode} = context; context.on('CallExpression', node => { if (node.optional) { return; } const {isTimer, name} = checkTimerCall(node, sourceCode); if (!isTimer) { return; } const {arguments: arguments_} = node; const hasDelayArgument = arguments_.length >= 2; if (mode === MODE_ALWAYS && !hasDelayArgument) { if (arguments_.length === 0) { return; } const [firstArgument] = arguments_; if (arguments_.length === 1 && firstArgument?.type === 'SpreadElement') { return; } return { node, messageId: MESSAGE_ID_MISSING_DELAY, data: {name}, fix: fixer => fixer.insertTextAfterRange( getParenthesizedRange(firstArgument, context), ', 0', ), }; } if (mode === MODE_NEVER && arguments_.length === 2) { const delayArgument = arguments_[1]; if (isZeroDelay(delayArgument)) { return { node: delayArgument, messageId: MESSAGE_ID_REDUNDANT_DELAY, data: {name}, fix(fixer) { const [firstArgument] = arguments_; const [, firstArgumentEnd] = getParenthesizedRange(firstArgument, context); const [, delayArgumentEnd] = getParenthesizedRange(delayArgument, context); return fixer.removeRange([firstArgumentEnd, delayArgumentEnd]); }, }; } } }); }; const schema = [ { enum: [MODE_ALWAYS, MODE_NEVER], description: 'Whether to always require an explicit `delay` argument, or disallow an explicit `0` delay.', }, ]; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'suggestion', docs: { description: 'Enforce or disallow explicit `delay` argument for `setTimeout()` and `setInterval()`.', recommended: 'unopinionated', }, fixable: 'code', schema, defaultOptions: [MODE_ALWAYS], messages, languages: [ 'js/js', ], }, }; export default config;