no-extra-boolean-cast.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. /**
  2. * @fileoverview Rule to flag unnecessary double negation in Boolean contexts
  3. * @author Brandon Mills
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. module.exports = {
  14. meta: {
  15. type: "suggestion",
  16. docs: {
  17. description: "disallow unnecessary boolean casts",
  18. category: "Possible Errors",
  19. recommended: true,
  20. url: "https://eslint.org/docs/rules/no-extra-boolean-cast"
  21. },
  22. schema: [],
  23. fixable: "code",
  24. messages: {
  25. unexpectedCall: "Redundant Boolean call.",
  26. unexpectedNegation: "Redundant double negation."
  27. }
  28. },
  29. create(context) {
  30. const sourceCode = context.getSourceCode();
  31. // Node types which have a test which will coerce values to booleans.
  32. const BOOLEAN_NODE_TYPES = [
  33. "IfStatement",
  34. "DoWhileStatement",
  35. "WhileStatement",
  36. "ConditionalExpression",
  37. "ForStatement"
  38. ];
  39. /**
  40. * Check if a node is in a context where its value would be coerced to a boolean at runtime.
  41. * @param {ASTNode} node The node
  42. * @param {ASTNode} parent Its parent
  43. * @returns {boolean} If it is in a boolean context
  44. */
  45. function isInBooleanContext(node, parent) {
  46. return (
  47. (BOOLEAN_NODE_TYPES.indexOf(parent.type) !== -1 &&
  48. node === parent.test) ||
  49. // !<bool>
  50. (parent.type === "UnaryExpression" &&
  51. parent.operator === "!")
  52. );
  53. }
  54. /**
  55. * Check if a node has comments inside.
  56. * @param {ASTNode} node The node to check.
  57. * @returns {boolean} `true` if it has comments inside.
  58. */
  59. function hasCommentsInside(node) {
  60. return Boolean(sourceCode.getCommentsInside(node).length);
  61. }
  62. return {
  63. UnaryExpression(node) {
  64. const ancestors = context.getAncestors(),
  65. parent = ancestors.pop(),
  66. grandparent = ancestors.pop();
  67. // Exit early if it's guaranteed not to match
  68. if (node.operator !== "!" ||
  69. parent.type !== "UnaryExpression" ||
  70. parent.operator !== "!") {
  71. return;
  72. }
  73. if (isInBooleanContext(parent, grandparent) ||
  74. // Boolean(<bool>) and new Boolean(<bool>)
  75. ((grandparent.type === "CallExpression" || grandparent.type === "NewExpression") &&
  76. grandparent.callee.type === "Identifier" &&
  77. grandparent.callee.name === "Boolean")
  78. ) {
  79. context.report({
  80. node: parent,
  81. messageId: "unexpectedNegation",
  82. fix: fixer => {
  83. if (hasCommentsInside(parent)) {
  84. return null;
  85. }
  86. let prefix = "";
  87. const tokenBefore = sourceCode.getTokenBefore(parent);
  88. const firstReplacementToken = sourceCode.getFirstToken(node.argument);
  89. if (tokenBefore && tokenBefore.range[1] === parent.range[0] &&
  90. !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)) {
  91. prefix = " ";
  92. }
  93. return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
  94. }
  95. });
  96. }
  97. },
  98. CallExpression(node) {
  99. const parent = node.parent;
  100. if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
  101. return;
  102. }
  103. if (isInBooleanContext(node, parent)) {
  104. context.report({
  105. node,
  106. messageId: "unexpectedCall",
  107. fix: fixer => {
  108. if (!node.arguments.length) {
  109. if (parent.type === "UnaryExpression" && parent.operator === "!") {
  110. // !Boolean() -> true
  111. if (hasCommentsInside(parent)) {
  112. return null;
  113. }
  114. const replacement = "true";
  115. let prefix = "";
  116. const tokenBefore = sourceCode.getTokenBefore(parent);
  117. if (tokenBefore && tokenBefore.range[1] === parent.range[0] &&
  118. !astUtils.canTokensBeAdjacent(tokenBefore, replacement)) {
  119. prefix = " ";
  120. }
  121. return fixer.replaceText(parent, prefix + replacement);
  122. }
  123. // Boolean() -> false
  124. if (hasCommentsInside(node)) {
  125. return null;
  126. }
  127. return fixer.replaceText(node, "false");
  128. }
  129. if (node.arguments.length > 1 || node.arguments[0].type === "SpreadElement" ||
  130. hasCommentsInside(node)) {
  131. return null;
  132. }
  133. const argument = node.arguments[0];
  134. if (astUtils.getPrecedence(argument) < astUtils.getPrecedence(node.parent)) {
  135. return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
  136. }
  137. return fixer.replaceText(node, sourceCode.getText(argument));
  138. }
  139. });
  140. }
  141. }
  142. };
  143. }
  144. };