no-unneeded-ternary.js 6.8 KB

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