/** * @fileoverview Rule to flag no-unneeded-ternary * @author Gyandeep Singh */ 'use strict'; const astUtils = require('./utils/ast-utils'); // Operators that always result in a boolean value const BOOLEAN_OPERATORS = new Set([ '==', '===', '!=', '!==', '>', '>=', '<', '<=', 'in', 'instanceof', ]); const OPERATOR_INVERSES = { '==': '!=', '!=': '==', '===': '!==', '!==': '===', // Operators like < and >= are not true inverses, since both will return false with NaN. }; const OR_PRECEDENCE = astUtils.getPrecedence({ type: 'LogicalExpression', operator: '||', }); //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', defaultOptions: [{ defaultAssignment: true }], docs: { description: 'Disallow ternary operators when simpler alternatives exist', recommended: false, frozen: true, url: 'https://eslint.org/docs/latest/rules/no-unneeded-ternary', }, schema: [ { type: 'object', properties: { defaultAssignment: { type: 'boolean', }, }, additionalProperties: false, }, ], fixable: 'code', messages: { unnecessaryConditionalExpression: 'Unnecessary use of boolean literals in conditional expression.', unnecessaryConditionalAssignment: 'Unnecessary use of conditional expression for default assignment.', }, }, create(context) { const [{ defaultAssignment }] = context.options; const sourceCode = context.sourceCode; /** * Test if the node is a boolean literal * @param {ASTNode} node The node to report. * @returns {boolean} True if the its a boolean literal * @private */ function isBooleanLiteral(node) { return node.type === 'Literal' && typeof node.value === 'boolean'; } /** * Creates an expression that represents the boolean inverse of the expression represented by the original node * @param {ASTNode} node A node representing an expression * @returns {string} A string representing an inverted expression */ function invertExpression(node) { if ( node.type === 'BinaryExpression' && Object.hasOwn(OPERATOR_INVERSES, node.operator) ) { const operatorToken = sourceCode.getFirstTokenBetween( node.left, node.right, (token) => token.value === node.operator ); const text = sourceCode.getText(); return ( text.slice(node.range[0], operatorToken.range[0]) + OPERATOR_INVERSES[node.operator] + text.slice(operatorToken.range[1], node.range[1]) ); } if ( astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: 'UnaryExpression' }) ) { return `!(${astUtils.getParenthesisedText(sourceCode, node)})`; } return `!${astUtils.getParenthesisedText(sourceCode, node)}`; } /** * Tests if a given node always evaluates to a boolean value * @param {ASTNode} node An expression node * @returns {boolean} True if it is determined that the node will always evaluate to a boolean value */ function isBooleanExpression(node) { return ( (node.type === 'BinaryExpression' && BOOLEAN_OPERATORS.has(node.operator)) || (node.type === 'UnaryExpression' && node.operator === '!') ); } /** * Test if the node matches the pattern id ? id : expression * @param {ASTNode} node The ConditionalExpression to check. * @returns {boolean} True if the pattern is matched, and false otherwise * @private */ function matchesDefaultAssignment(node) { return ( node.test.type === 'Identifier' && node.consequent.type === 'Identifier' && node.test.name === node.consequent.name ); } return { ConditionalExpression(node) { if ( isBooleanLiteral(node.alternate) && isBooleanLiteral(node.consequent) ) { context.report({ node, messageId: 'unnecessaryConditionalExpression', fix(fixer) { if (node.consequent.value === node.alternate.value) { // Replace `foo ? true : true` with just `true`, but don't replace `foo() ? true : true` return node.test.type === 'Identifier' ? fixer.replaceText(node, node.consequent.value.toString()) : null; } if (node.alternate.value) { // Replace `foo() ? false : true` with `!(foo())` return fixer.replaceText(node, invertExpression(node.test)); } // Replace `foo ? true : false` with `foo` if `foo` is guaranteed to be a boolean, or `!!foo` otherwise. return fixer.replaceText( node, isBooleanExpression(node.test) ? astUtils.getParenthesisedText(sourceCode, node.test) : `!${invertExpression(node.test)}` ); }, }); } else if (!defaultAssignment && matchesDefaultAssignment(node)) { context.report({ node, messageId: 'unnecessaryConditionalAssignment', fix(fixer) { const shouldParenthesizeAlternate = (astUtils.getPrecedence(node.alternate) < OR_PRECEDENCE || astUtils.isCoalesceExpression(node.alternate)) && !astUtils.isParenthesised(sourceCode, node.alternate); const alternateText = shouldParenthesizeAlternate ? `(${sourceCode.getText(node.alternate)})` : astUtils.getParenthesisedText(sourceCode, node.alternate); const testText = astUtils.getParenthesisedText( sourceCode, node.test ); return fixer.replaceText(node, `${testText} || ${alternateText}`); }, }); } }, }; }, };