You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
166 lines
6.8 KiB
166 lines
6.8 KiB
/** |
|
* @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('../shared/types').Rule} */ |
|
module.exports = { |
|
meta: { |
|
type: "suggestion", |
|
|
|
docs: { |
|
description: "Disallow ternary operators when simpler alternatives exist", |
|
recommended: false, |
|
url: "https://eslint.org/docs/rules/no-unneeded-ternary" |
|
}, |
|
|
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
defaultAssignment: { |
|
type: "boolean", |
|
default: true |
|
} |
|
}, |
|
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 options = context.options[0] || {}; |
|
const defaultAssignment = options.defaultAssignment !== false; |
|
const sourceCode = context.getSourceCode(); |
|
|
|
/** |
|
* 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.prototype.hasOwnProperty.call(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}`); |
|
} |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
};
|
|
|