1/** 2 * @fileoverview Rule to flag no-unneeded-ternary 3 * @author Gyandeep Singh 4 */ 5 6"use strict"; 7 8const astUtils = require("./utils/ast-utils"); 9 10// Operators that always result in a boolean value 11const BOOLEAN_OPERATORS = new Set(["==", "===", "!=", "!==", ">", ">=", "<", "<=", "in", "instanceof"]); 12const OPERATOR_INVERSES = { 13 "==": "!=", 14 "!=": "==", 15 "===": "!==", 16 "!==": "===" 17 18 // Operators like < and >= are not true inverses, since both will return false with NaN. 19}; 20const OR_PRECEDENCE = astUtils.getPrecedence({ type: "LogicalExpression", operator: "||" }); 21 22//------------------------------------------------------------------------------ 23// Rule Definition 24//------------------------------------------------------------------------------ 25 26module.exports = { 27 meta: { 28 type: "suggestion", 29 30 docs: { 31 description: "disallow ternary operators when simpler alternatives exist", 32 category: "Stylistic Issues", 33 recommended: false, 34 url: "https://eslint.org/docs/rules/no-unneeded-ternary" 35 }, 36 37 schema: [ 38 { 39 type: "object", 40 properties: { 41 defaultAssignment: { 42 type: "boolean", 43 default: true 44 } 45 }, 46 additionalProperties: false 47 } 48 ], 49 50 fixable: "code", 51 52 messages: { 53 unnecessaryConditionalExpression: "Unnecessary use of boolean literals in conditional expression.", 54 unnecessaryConditionalAssignment: "Unnecessary use of conditional expression for default assignment." 55 } 56 }, 57 58 create(context) { 59 const options = context.options[0] || {}; 60 const defaultAssignment = options.defaultAssignment !== false; 61 const sourceCode = context.getSourceCode(); 62 63 /** 64 * Test if the node is a boolean literal 65 * @param {ASTNode} node The node to report. 66 * @returns {boolean} True if the its a boolean literal 67 * @private 68 */ 69 function isBooleanLiteral(node) { 70 return node.type === "Literal" && typeof node.value === "boolean"; 71 } 72 73 /** 74 * Creates an expression that represents the boolean inverse of the expression represented by the original node 75 * @param {ASTNode} node A node representing an expression 76 * @returns {string} A string representing an inverted expression 77 */ 78 function invertExpression(node) { 79 if (node.type === "BinaryExpression" && Object.prototype.hasOwnProperty.call(OPERATOR_INVERSES, node.operator)) { 80 const operatorToken = sourceCode.getFirstTokenBetween( 81 node.left, 82 node.right, 83 token => token.value === node.operator 84 ); 85 const text = sourceCode.getText(); 86 87 return text.slice(node.range[0], 88 operatorToken.range[0]) + OPERATOR_INVERSES[node.operator] + text.slice(operatorToken.range[1], node.range[1]); 89 } 90 91 if (astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression" })) { 92 return `!(${astUtils.getParenthesisedText(sourceCode, node)})`; 93 } 94 return `!${astUtils.getParenthesisedText(sourceCode, node)}`; 95 } 96 97 /** 98 * Tests if a given node always evaluates to a boolean value 99 * @param {ASTNode} node An expression node 100 * @returns {boolean} True if it is determined that the node will always evaluate to a boolean value 101 */ 102 function isBooleanExpression(node) { 103 return node.type === "BinaryExpression" && BOOLEAN_OPERATORS.has(node.operator) || 104 node.type === "UnaryExpression" && node.operator === "!"; 105 } 106 107 /** 108 * Test if the node matches the pattern id ? id : expression 109 * @param {ASTNode} node The ConditionalExpression to check. 110 * @returns {boolean} True if the pattern is matched, and false otherwise 111 * @private 112 */ 113 function matchesDefaultAssignment(node) { 114 return node.test.type === "Identifier" && 115 node.consequent.type === "Identifier" && 116 node.test.name === node.consequent.name; 117 } 118 119 return { 120 121 ConditionalExpression(node) { 122 if (isBooleanLiteral(node.alternate) && isBooleanLiteral(node.consequent)) { 123 context.report({ 124 node, 125 messageId: "unnecessaryConditionalExpression", 126 fix(fixer) { 127 if (node.consequent.value === node.alternate.value) { 128 129 // Replace `foo ? true : true` with just `true`, but don't replace `foo() ? true : true` 130 return node.test.type === "Identifier" ? fixer.replaceText(node, node.consequent.value.toString()) : null; 131 } 132 if (node.alternate.value) { 133 134 // Replace `foo() ? false : true` with `!(foo())` 135 return fixer.replaceText(node, invertExpression(node.test)); 136 } 137 138 // Replace `foo ? true : false` with `foo` if `foo` is guaranteed to be a boolean, or `!!foo` otherwise. 139 140 return fixer.replaceText(node, isBooleanExpression(node.test) ? astUtils.getParenthesisedText(sourceCode, node.test) : `!${invertExpression(node.test)}`); 141 } 142 }); 143 } else if (!defaultAssignment && matchesDefaultAssignment(node)) { 144 context.report({ 145 node, 146 messageId: "unnecessaryConditionalAssignment", 147 fix: fixer => { 148 const shouldParenthesizeAlternate = 149 ( 150 astUtils.getPrecedence(node.alternate) < OR_PRECEDENCE || 151 astUtils.isCoalesceExpression(node.alternate) 152 ) && 153 !astUtils.isParenthesised(sourceCode, node.alternate); 154 const alternateText = shouldParenthesizeAlternate 155 ? `(${sourceCode.getText(node.alternate)})` 156 : astUtils.getParenthesisedText(sourceCode, node.alternate); 157 const testText = astUtils.getParenthesisedText(sourceCode, node.test); 158 159 return fixer.replaceText(node, `${testText} || ${alternateText}`); 160 } 161 }); 162 } 163 } 164 }; 165 } 166}; 167