no-unneeded-ternary.js 5.8 KB

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