no-extra-boolean-cast.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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. const eslintUtils = require("eslint-utils");
  11. const precedence = astUtils.getPrecedence;
  12. //------------------------------------------------------------------------------
  13. // Rule Definition
  14. //------------------------------------------------------------------------------
  15. module.exports = {
  16. meta: {
  17. type: "suggestion",
  18. docs: {
  19. description: "disallow unnecessary boolean casts",
  20. category: "Possible Errors",
  21. recommended: true,
  22. url: "https://eslint.org/docs/rules/no-extra-boolean-cast"
  23. },
  24. schema: [{
  25. type: "object",
  26. properties: {
  27. enforceForLogicalOperands: {
  28. type: "boolean",
  29. default: false
  30. }
  31. },
  32. additionalProperties: false
  33. }],
  34. fixable: "code",
  35. messages: {
  36. unexpectedCall: "Redundant Boolean call.",
  37. unexpectedNegation: "Redundant double negation."
  38. }
  39. },
  40. create(context) {
  41. const sourceCode = context.getSourceCode();
  42. // Node types which have a test which will coerce values to booleans.
  43. const BOOLEAN_NODE_TYPES = [
  44. "IfStatement",
  45. "DoWhileStatement",
  46. "WhileStatement",
  47. "ConditionalExpression",
  48. "ForStatement"
  49. ];
  50. /**
  51. * Check if a node is a Boolean function or constructor.
  52. * @param {ASTNode} node the node
  53. * @returns {boolean} If the node is Boolean function or constructor
  54. */
  55. function isBooleanFunctionOrConstructorCall(node) {
  56. // Boolean(<bool>) and new Boolean(<bool>)
  57. return (node.type === "CallExpression" || node.type === "NewExpression") &&
  58. node.callee.type === "Identifier" &&
  59. node.callee.name === "Boolean";
  60. }
  61. /**
  62. * Checks whether the node is a logical expression and that the option is enabled
  63. * @param {ASTNode} node the node
  64. * @returns {boolean} if the node is a logical expression and option is enabled
  65. */
  66. function isLogicalContext(node) {
  67. return node.type === "LogicalExpression" &&
  68. (node.operator === "||" || node.operator === "&&") &&
  69. (context.options.length && context.options[0].enforceForLogicalOperands === true);
  70. }
  71. /**
  72. * Check if a node is in a context where its value would be coerced to a boolean at runtime.
  73. * @param {ASTNode} node The node
  74. * @returns {boolean} If it is in a boolean context
  75. */
  76. function isInBooleanContext(node) {
  77. return (
  78. (isBooleanFunctionOrConstructorCall(node.parent) &&
  79. node === node.parent.arguments[0]) ||
  80. (BOOLEAN_NODE_TYPES.indexOf(node.parent.type) !== -1 &&
  81. node === node.parent.test) ||
  82. // !<bool>
  83. (node.parent.type === "UnaryExpression" &&
  84. node.parent.operator === "!")
  85. );
  86. }
  87. /**
  88. * Checks whether the node is a context that should report an error
  89. * Acts recursively if it is in a logical context
  90. * @param {ASTNode} node the node
  91. * @returns {boolean} If the node is in one of the flagged contexts
  92. */
  93. function isInFlaggedContext(node) {
  94. if (node.parent.type === "ChainExpression") {
  95. return isInFlaggedContext(node.parent);
  96. }
  97. return isInBooleanContext(node) ||
  98. (isLogicalContext(node.parent) &&
  99. // For nested logical statements
  100. isInFlaggedContext(node.parent)
  101. );
  102. }
  103. /**
  104. * Check if a node has comments inside.
  105. * @param {ASTNode} node The node to check.
  106. * @returns {boolean} `true` if it has comments inside.
  107. */
  108. function hasCommentsInside(node) {
  109. return Boolean(sourceCode.getCommentsInside(node).length);
  110. }
  111. /**
  112. * Checks if the given node is wrapped in grouping parentheses. Parentheses for constructs such as if() don't count.
  113. * @param {ASTNode} node The node to check.
  114. * @returns {boolean} `true` if the node is parenthesized.
  115. * @private
  116. */
  117. function isParenthesized(node) {
  118. return eslintUtils.isParenthesized(1, node, sourceCode);
  119. }
  120. /**
  121. * Determines whether the given node needs to be parenthesized when replacing the previous node.
  122. * It assumes that `previousNode` is the node to be reported by this rule, so it has a limited list
  123. * of possible parent node types. By the same assumption, the node's role in a particular parent is already known.
  124. * For example, if the parent is `ConditionalExpression`, `previousNode` must be its `test` child.
  125. * @param {ASTNode} previousNode Previous node.
  126. * @param {ASTNode} node The node to check.
  127. * @returns {boolean} `true` if the node needs to be parenthesized.
  128. */
  129. function needsParens(previousNode, node) {
  130. if (previousNode.parent.type === "ChainExpression") {
  131. return needsParens(previousNode.parent, node);
  132. }
  133. if (isParenthesized(previousNode)) {
  134. // parentheses around the previous node will stay, so there is no need for an additional pair
  135. return false;
  136. }
  137. // parent of the previous node will become parent of the replacement node
  138. const parent = previousNode.parent;
  139. switch (parent.type) {
  140. case "CallExpression":
  141. case "NewExpression":
  142. return node.type === "SequenceExpression";
  143. case "IfStatement":
  144. case "DoWhileStatement":
  145. case "WhileStatement":
  146. case "ForStatement":
  147. return false;
  148. case "ConditionalExpression":
  149. return precedence(node) <= precedence(parent);
  150. case "UnaryExpression":
  151. return precedence(node) < precedence(parent);
  152. case "LogicalExpression":
  153. if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) {
  154. return true;
  155. }
  156. if (previousNode === parent.left) {
  157. return precedence(node) < precedence(parent);
  158. }
  159. return precedence(node) <= precedence(parent);
  160. /* istanbul ignore next */
  161. default:
  162. throw new Error(`Unexpected parent type: ${parent.type}`);
  163. }
  164. }
  165. return {
  166. UnaryExpression(node) {
  167. const parent = node.parent;
  168. // Exit early if it's guaranteed not to match
  169. if (node.operator !== "!" ||
  170. parent.type !== "UnaryExpression" ||
  171. parent.operator !== "!") {
  172. return;
  173. }
  174. if (isInFlaggedContext(parent)) {
  175. context.report({
  176. node: parent,
  177. messageId: "unexpectedNegation",
  178. fix(fixer) {
  179. if (hasCommentsInside(parent)) {
  180. return null;
  181. }
  182. if (needsParens(parent, node.argument)) {
  183. return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`);
  184. }
  185. let prefix = "";
  186. const tokenBefore = sourceCode.getTokenBefore(parent);
  187. const firstReplacementToken = sourceCode.getFirstToken(node.argument);
  188. if (
  189. tokenBefore &&
  190. tokenBefore.range[1] === parent.range[0] &&
  191. !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
  192. ) {
  193. prefix = " ";
  194. }
  195. return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
  196. }
  197. });
  198. }
  199. },
  200. CallExpression(node) {
  201. if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
  202. return;
  203. }
  204. if (isInFlaggedContext(node)) {
  205. context.report({
  206. node,
  207. messageId: "unexpectedCall",
  208. fix(fixer) {
  209. const parent = node.parent;
  210. if (node.arguments.length === 0) {
  211. if (parent.type === "UnaryExpression" && parent.operator === "!") {
  212. /*
  213. * !Boolean() -> true
  214. */
  215. if (hasCommentsInside(parent)) {
  216. return null;
  217. }
  218. const replacement = "true";
  219. let prefix = "";
  220. const tokenBefore = sourceCode.getTokenBefore(parent);
  221. if (
  222. tokenBefore &&
  223. tokenBefore.range[1] === parent.range[0] &&
  224. !astUtils.canTokensBeAdjacent(tokenBefore, replacement)
  225. ) {
  226. prefix = " ";
  227. }
  228. return fixer.replaceText(parent, prefix + replacement);
  229. }
  230. /*
  231. * Boolean() -> false
  232. */
  233. if (hasCommentsInside(node)) {
  234. return null;
  235. }
  236. return fixer.replaceText(node, "false");
  237. }
  238. if (node.arguments.length === 1) {
  239. const argument = node.arguments[0];
  240. if (argument.type === "SpreadElement" || hasCommentsInside(node)) {
  241. return null;
  242. }
  243. /*
  244. * Boolean(expression) -> expression
  245. */
  246. if (needsParens(node, argument)) {
  247. return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
  248. }
  249. return fixer.replaceText(node, sourceCode.getText(argument));
  250. }
  251. // two or more arguments
  252. return null;
  253. }
  254. });
  255. }
  256. }
  257. };
  258. }
  259. };