[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Rule to flag no-unneeded-ternary
|
---|
| 3 | * @author Gyandeep Singh
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | "use strict";
|
---|
| 7 |
|
---|
| 8 | const astUtils = require("./utils/ast-utils");
|
---|
| 9 |
|
---|
| 10 | // Operators that always result in a boolean value
|
---|
| 11 | const BOOLEAN_OPERATORS = new Set(["==", "===", "!=", "!==", ">", ">=", "<", "<=", "in", "instanceof"]);
|
---|
| 12 | const OPERATOR_INVERSES = {
|
---|
| 13 | "==": "!=",
|
---|
| 14 | "!=": "==",
|
---|
| 15 | "===": "!==",
|
---|
| 16 | "!==": "==="
|
---|
| 17 |
|
---|
| 18 | // Operators like < and >= are not true inverses, since both will return false with NaN.
|
---|
| 19 | };
|
---|
| 20 | const OR_PRECEDENCE = astUtils.getPrecedence({ type: "LogicalExpression", operator: "||" });
|
---|
| 21 |
|
---|
| 22 | //------------------------------------------------------------------------------
|
---|
| 23 | // Rule Definition
|
---|
| 24 | //------------------------------------------------------------------------------
|
---|
| 25 |
|
---|
| 26 | /** @type {import('../shared/types').Rule} */
|
---|
| 27 | module.exports = {
|
---|
| 28 | meta: {
|
---|
| 29 | type: "suggestion",
|
---|
| 30 |
|
---|
| 31 | docs: {
|
---|
| 32 | description: "Disallow ternary operators when simpler alternatives exist",
|
---|
| 33 | recommended: false,
|
---|
| 34 | url: "https://eslint.org/docs/latest/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.sourceCode;
|
---|
| 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 | };
|
---|