UNPKG

eslint-plugin-unicorn

Version:
242 lines (207 loc) 5.65 kB
import {findVariable} from '@eslint-community/eslint-utils'; import { getLastTrailingCommentOnSameLine, getNextNode, getParenthesizedText, getPreviousNode, isGlobalBooleanCall, shouldAddParenthesesToUnaryExpressionArgument, } 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) { /* c8 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' && node.argument?.type === 'Literal' && typeof node.argument.value === 'boolean' ) { return node.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 canUseGlobalBoolean(node) { const variable = findVariable(sourceCode.getScope(node), 'Boolean'); return !variable || variable.defs.length === 0; } function getBooleanReturnText(node, shouldNegateTest) { const text = getParenthesizedText(node, context); if (shouldNegateTest) { return shouldAddParenthesesToUnaryExpressionArgument(node, '!') ? `!(${text})` : `!${text}`; } if (isRuntimeBooleanExpression(node, context)) { return text; } const booleanArgumentText = node.type === 'SequenceExpression' ? `(${text})` : text; return `Boolean(${booleanArgumentText})`; } 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) && !canUseGlobalBoolean(node); 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; } const hasCommentsBetween = sourceCode.getTokensBetween(node, alternate, {includeComments: true}) .some(token => token.type === 'Block' || token.type === 'Line'); return hasCommentsBetween ? {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;