UNPKG

eslint-plugin-unicorn

Version:
238 lines (204 loc) • 5.37 kB
import { getCallArgumentText, getLastTrailingCommentOnSameLine, getNegatedExpressionText, getNextNode, getParenthesizedText, getPreviousNode, isGlobalBooleanCall, isGlobalNameAvailable, unwrapTypeScriptExpression, } from './utils/index.js'; const MESSAGE_ID = 'prefer-boolean-return'; const messages = { [MESSAGE_ID]: 'Return the boolean expression directly.', }; const runtimeBooleanBinaryOperators = new Set([ '==', '!=', '===', '!==', '<', '<=', '>', '>=', 'in', 'instanceof', ]); function getNodeBody(node) { /* node:coverage ignore next 3 */ if (!node) { return; } if (node.type === 'BlockStatement') { const body = node.body.filter(({type}) => type !== 'EmptyStatement'); if (body.length === 1) { return getNodeBody(body[0]); } } return node; } const getBooleanReturnValue = node => { if (node?.type !== 'ReturnStatement') { return; } // `as`, `satisfies` and `!` have no runtime effect, so the value is still the literal const argument = unwrapTypeScriptExpression(node.argument); if (argument?.type === 'Literal' && typeof argument.value === 'boolean') { return argument.value; } }; function isGlobalArrayIsArrayCall(node, context) { return node.type === 'CallExpression' && !node.optional && node.callee.type === 'MemberExpression' && !node.callee.optional && !node.callee.computed && node.callee.object.type === 'Identifier' && node.callee.object.name === 'Array' && context.sourceCode.isGlobalReference(node.callee.object) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'isArray'; } function isRuntimeBooleanExpression(node, context) { switch (node.type) { case 'Literal': { return typeof node.value === 'boolean'; } case 'UnaryExpression': { return ['!', 'delete'].includes(node.operator); } case 'BinaryExpression': { return runtimeBooleanBinaryOperators.has(node.operator); } case 'LogicalExpression': { return isRuntimeBooleanExpression(node.left, context) && isRuntimeBooleanExpression(node.right, context); } case 'CallExpression': { return isGlobalBooleanCall(node, context) || isGlobalArrayIsArrayCall(node, context); } default: { return false; } } } /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const {sourceCode} = context; function getBooleanReturnText(node, shouldNegateTest) { if (shouldNegateTest) { return getNegatedExpressionText(node, context); } if (isRuntimeBooleanExpression(node, context)) { return getParenthesizedText(node, context); } return `Boolean(${getCallArgumentText(node, context)})`; } function getProblem(node, consequent, alternate, replacementRange = sourceCode.getRange(node)) { if ( !consequent || !alternate || node.test.type === 'ConditionalExpression' ) { return; } const consequentValue = getBooleanReturnValue(consequent); const alternateValue = getBooleanReturnValue(alternate); if ( consequentValue === undefined || alternateValue === undefined || consequentValue === alternateValue ) { return; } const problem = {node, messageId: MESSAGE_ID}; const shouldNegateTest = !consequentValue; const hasComments = sourceCode.getCommentsInside(node).length > 0 || sourceCode.getCommentsInside(alternate).length > 0 || getLastTrailingCommentOnSameLine(context, alternate); const isNeedsGlobalBoolean = !shouldNegateTest && !isRuntimeBooleanExpression(node.test, context) && !isGlobalNameAvailable('Boolean', node, context); if (hasComments || isNeedsGlobalBoolean) { return problem; } return { ...problem, fix: fixer => fixer.replaceTextRange( replacementRange, `return ${getBooleanReturnText(node.test, shouldNegateTest)};`, ), }; } function isPrecededByFlatIfReturningSameBoolean(node, returnValue) { const previousNode = getPreviousNode(node, context); return previousNode?.type === 'IfStatement' && !previousNode.alternate && getBooleanReturnValue(getNodeBody(previousNode.consequent)) === returnValue; } function getFlatProblem(node) { const alternate = getNextNode(node, context); if ( !alternate || alternate.type !== 'ReturnStatement' ) { return; } const consequent = getNodeBody(node.consequent); const consequentValue = getBooleanReturnValue(consequent); if ( consequentValue !== undefined && isPrecededByFlatIfReturningSameBoolean(node, consequentValue) ) { return; } const problem = getProblem( node, consequent, alternate, [ sourceCode.getRange(node)[0], sourceCode.getRange(alternate)[1], ], ); if (!problem?.fix) { return problem; } return sourceCode.commentsExistBetween(node, alternate) ? {node, messageId: MESSAGE_ID} : problem; } context.on('IfStatement', node => { if (!node.alternate) { return getFlatProblem(node); } return getProblem( node, getNodeBody(node.consequent), getNodeBody(node.alternate), ); }); }; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'suggestion', docs: { description: 'Prefer directly returning boolean expressions over `if` statements.', recommended: 'unopinionated', }, fixable: 'code', schema: [], messages, languages: [ 'js/js', ], }, }; export default config;