no-extra-boolean-cast.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  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. /** @type {import('../shared/types').Rule} */
  16. module.exports = {
  17. meta: {
  18. type: "suggestion",
  19. docs: {
  20. description: "Disallow unnecessary boolean casts",
  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 = new Set([
  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.has(node.parent.type) &&
  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. * @throws {Error} (Unreachable.)
  128. * @returns {boolean} `true` if the node needs to be parenthesized.
  129. */
  130. function needsParens(previousNode, node) {
  131. if (previousNode.parent.type === "ChainExpression") {
  132. return needsParens(previousNode.parent, node);
  133. }
  134. if (isParenthesized(previousNode)) {
  135. // parentheses around the previous node will stay, so there is no need for an additional pair
  136. return false;
  137. }
  138. // parent of the previous node will become parent of the replacement node
  139. const parent = previousNode.parent;
  140. switch (parent.type) {
  141. case "CallExpression":
  142. case "NewExpression":
  143. return node.type === "SequenceExpression";
  144. case "IfStatement":
  145. case "DoWhileStatement":
  146. case "WhileStatement":
  147. case "ForStatement":
  148. return false;
  149. case "ConditionalExpression":
  150. return precedence(node) <= precedence(parent);
  151. case "UnaryExpression":
  152. return precedence(node) < precedence(parent);
  153. case "LogicalExpression":
  154. if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) {
  155. return true;
  156. }
  157. if (previousNode === parent.left) {
  158. return precedence(node) < precedence(parent);
  159. }
  160. return precedence(node) <= precedence(parent);
  161. /* c8 ignore next */
  162. default:
  163. throw new Error(`Unexpected parent type: ${parent.type}`);
  164. }
  165. }
  166. return {
  167. UnaryExpression(node) {
  168. const parent = node.parent;
  169. // Exit early if it's guaranteed not to match
  170. if (node.operator !== "!" ||
  171. parent.type !== "UnaryExpression" ||
  172. parent.operator !== "!") {
  173. return;
  174. }
  175. if (isInFlaggedContext(parent)) {
  176. context.report({
  177. node: parent,
  178. messageId: "unexpectedNegation",
  179. fix(fixer) {
  180. if (hasCommentsInside(parent)) {
  181. return null;
  182. }
  183. if (needsParens(parent, node.argument)) {
  184. return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`);
  185. }
  186. let prefix = "";
  187. const tokenBefore = sourceCode.getTokenBefore(parent);
  188. const firstReplacementToken = sourceCode.getFirstToken(node.argument);
  189. if (
  190. tokenBefore &&
  191. tokenBefore.range[1] === parent.range[0] &&
  192. !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
  193. ) {
  194. prefix = " ";
  195. }
  196. return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
  197. }
  198. });
  199. }
  200. },
  201. CallExpression(node) {
  202. if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
  203. return;
  204. }
  205. if (isInFlaggedContext(node)) {
  206. context.report({
  207. node,
  208. messageId: "unexpectedCall",
  209. fix(fixer) {
  210. const parent = node.parent;
  211. if (node.arguments.length === 0) {
  212. if (parent.type === "UnaryExpression" && parent.operator === "!") {
  213. /*
  214. * !Boolean() -> true
  215. */
  216. if (hasCommentsInside(parent)) {
  217. return null;
  218. }
  219. const replacement = "true";
  220. let prefix = "";
  221. const tokenBefore = sourceCode.getTokenBefore(parent);
  222. if (
  223. tokenBefore &&
  224. tokenBefore.range[1] === parent.range[0] &&
  225. !astUtils.canTokensBeAdjacent(tokenBefore, replacement)
  226. ) {
  227. prefix = " ";
  228. }
  229. return fixer.replaceText(parent, prefix + replacement);
  230. }
  231. /*
  232. * Boolean() -> false
  233. */
  234. if (hasCommentsInside(node)) {
  235. return null;
  236. }
  237. return fixer.replaceText(node, "false");
  238. }
  239. if (node.arguments.length === 1) {
  240. const argument = node.arguments[0];
  241. if (argument.type === "SpreadElement" || hasCommentsInside(node)) {
  242. return null;
  243. }
  244. /*
  245. * Boolean(expression) -> expression
  246. */
  247. if (needsParens(node, argument)) {
  248. return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
  249. }
  250. return fixer.replaceText(node, sourceCode.getText(argument));
  251. }
  252. // two or more arguments
  253. return null;
  254. }
  255. });
  256. }
  257. }
  258. };
  259. }
  260. };