UNPKG

eslint-plugin-unicorn

Version:
203 lines (169 loc) • 5.06 kB
import {isBooleanLiteral, isInDirectivePrologue, isStringLiteral} from './ast/index.js'; import replaceNodeWithExpression from './fix/replace-node-with-expression.js'; import { getLogicalExpressionChildText, getLogicalExpressionOperands, isBoolean, isBooleanContext, isOutermostLogicalExpression, isParenthesized, needsSemicolon, unwrapTypeScriptExpression, } from './utils/index.js'; /** @import {TSESTree as ESTree} from '@typescript-eslint/types'; @import * as ESLint from 'eslint'; */ const MESSAGE_ID = 'no-useless-logical-operand'; const messages = { [MESSAGE_ID]: 'Simplify this `{{operator}}` expression.', }; const identityByOperator = new Map([ ['&&', true], ['||', false], ]); const absorbingByOperator = new Map([ ['&&', false], ['||', true], ]); const leadingOperandTypesNeedingParentheses = new Set([ 'FunctionExpression', 'ClassExpression', 'ObjectExpression', ]); function areKnownBooleanOperands(operands, context) { return operands.every(operand => isBoolean(operand, context)); } function isRemovableIdentityOperand(operands, index, context) { if (index < operands.length - 1) { return true; } const remainingOperands = operands.slice(0, -1); const {parent} = operands[index]; return isBooleanContext(parent, context) || areKnownBooleanOperands(remainingOperands, context); } function getLeadingAbsorbingOperand(operands, operator) { const absorbingValue = absorbingByOperator.get(operator); // `true as boolean` and `true!` are the literal at runtime return isBooleanLiteral(unwrapTypeScriptExpression(operands[0]), absorbingValue) ? operands[0] : undefined; } function getRemovableIdentityOperands(operands, operator, context) { const identityValue = identityByOperator.get(operator); // `true as boolean` and `true!` are the literal at runtime return operands.filter((operand, index) => isBooleanLiteral(unwrapTypeScriptExpression(operand), identityValue) && isRemovableIdentityOperand(operands, index, context), ); } function needsLeadingOperandParentheses(operand) { return leadingOperandTypesNeedingParentheses.has(unwrapTypeScriptExpression(operand).type); } function isStringLiteralExpression(node) { return isStringLiteral(unwrapTypeScriptExpression(node)); } function getOperandText(operand, operator, index, context) { const text = getLogicalExpressionChildText(operand, context, { operator, property: index === 0 ? 'left' : 'right', }); if ( index === 0 && !isParenthesized(operand, context) && needsLeadingOperandParentheses(operand) ) { return `(${text})`; } return text; } function getReplacementText(node, replacementOperands, operator, context) { const replacement = replacementOperands .map((operand, index) => getOperandText(operand, operator, index, context)) .join(` ${operator} `); return needsSemicolon(context.sourceCode.getTokenBefore(node), context, replacement) ? `;${replacement}` : replacement; } function getProblem(node, context) { const {operator} = node; const operands = getLogicalExpressionOperands(node, operator); const leadingAbsorbingOperand = getLeadingAbsorbingOperand(operands, operator); if (leadingAbsorbingOperand) { return { target: leadingAbsorbingOperand, replacementOperands: [leadingAbsorbingOperand], }; } const removableOperands = getRemovableIdentityOperands(operands, operator, context); if (removableOperands.length === 0) { return; } const replacementOperands = operands.filter(operand => !removableOperands.includes(operand)); return { target: removableOperands[0], replacementOperands: replacementOperands.length === 0 ? [removableOperands.at(-1)] : replacementOperands, }; } /** @param {ESLint.Rule.RuleContext} context */ const create = context => { context.on('LogicalExpression', node => { if ( !identityByOperator.has(node.operator) || !isOutermostLogicalExpression(node) ) { return; } const problem = getProblem(node, context); if (!problem) { return; } const {target, replacementOperands} = problem; if ( replacementOperands.length === 1 && isStringLiteralExpression(replacementOperands[0]) && isInDirectivePrologue(node) ) { return; } const report = { node: target, messageId: MESSAGE_ID, data: { operator: node.operator, }, }; if (context.sourceCode.getCommentsInside(node).length > 0) { return report; } if (replacementOperands.length === 1) { report.fix = fixer => replaceNodeWithExpression(fixer, node, replacementOperands[0], context); return report; } report.fix = fixer => fixer.replaceText( node, getReplacementText(node, replacementOperands, node.operator, context), ); return report; }); }; /** @type {ESLint.Rule.RuleModule} */ const config = { create, meta: { type: 'suggestion', docs: { description: 'Disallow unnecessary operands in logical expressions involving boolean literals.', recommended: 'unopinionated', }, fixable: 'code', schema: [], messages, languages: [ 'js/js', ], }, }; export default config;