no-extra-boolean-cast.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  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-community/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/latest/rules/no-extra-boolean-cast"
  23. },
  24. schema: [{
  25. anyOf: [
  26. {
  27. type: "object",
  28. properties: {
  29. enforceForInnerExpressions: {
  30. type: "boolean"
  31. }
  32. },
  33. additionalProperties: false
  34. },
  35. // deprecated
  36. {
  37. type: "object",
  38. properties: {
  39. enforceForLogicalOperands: {
  40. type: "boolean"
  41. }
  42. },
  43. additionalProperties: false
  44. }
  45. ]
  46. }],
  47. fixable: "code",
  48. messages: {
  49. unexpectedCall: "Redundant Boolean call.",
  50. unexpectedNegation: "Redundant double negation."
  51. }
  52. },
  53. create(context) {
  54. const sourceCode = context.sourceCode;
  55. const enforceForLogicalOperands = context.options[0]?.enforceForLogicalOperands === true;
  56. const enforceForInnerExpressions = context.options[0]?.enforceForInnerExpressions === true;
  57. // Node types which have a test which will coerce values to booleans.
  58. const BOOLEAN_NODE_TYPES = new Set([
  59. "IfStatement",
  60. "DoWhileStatement",
  61. "WhileStatement",
  62. "ConditionalExpression",
  63. "ForStatement"
  64. ]);
  65. /**
  66. * Check if a node is a Boolean function or constructor.
  67. * @param {ASTNode} node the node
  68. * @returns {boolean} If the node is Boolean function or constructor
  69. */
  70. function isBooleanFunctionOrConstructorCall(node) {
  71. // Boolean(<bool>) and new Boolean(<bool>)
  72. return (node.type === "CallExpression" || node.type === "NewExpression") &&
  73. node.callee.type === "Identifier" &&
  74. node.callee.name === "Boolean";
  75. }
  76. /**
  77. * Check if a node is in a context where its value would be coerced to a boolean at runtime.
  78. * @param {ASTNode} node The node
  79. * @returns {boolean} If it is in a boolean context
  80. */
  81. function isInBooleanContext(node) {
  82. return (
  83. (isBooleanFunctionOrConstructorCall(node.parent) &&
  84. node === node.parent.arguments[0]) ||
  85. (BOOLEAN_NODE_TYPES.has(node.parent.type) &&
  86. node === node.parent.test) ||
  87. // !<bool>
  88. (node.parent.type === "UnaryExpression" &&
  89. node.parent.operator === "!")
  90. );
  91. }
  92. /**
  93. * Checks whether the node is a context that should report an error
  94. * Acts recursively if it is in a logical context
  95. * @param {ASTNode} node the node
  96. * @returns {boolean} If the node is in one of the flagged contexts
  97. */
  98. function isInFlaggedContext(node) {
  99. if (node.parent.type === "ChainExpression") {
  100. return isInFlaggedContext(node.parent);
  101. }
  102. /*
  103. * legacy behavior - enforceForLogicalOperands will only recurse on
  104. * logical expressions, not on other contexts.
  105. * enforceForInnerExpressions will recurse on logical expressions
  106. * as well as the other recursive syntaxes.
  107. */
  108. if (enforceForLogicalOperands || enforceForInnerExpressions) {
  109. if (node.parent.type === "LogicalExpression") {
  110. if (node.parent.operator === "||" || node.parent.operator === "&&") {
  111. return isInFlaggedContext(node.parent);
  112. }
  113. // Check the right hand side of a `??` operator.
  114. if (enforceForInnerExpressions &&
  115. node.parent.operator === "??" &&
  116. node.parent.right === node
  117. ) {
  118. return isInFlaggedContext(node.parent);
  119. }
  120. }
  121. }
  122. if (enforceForInnerExpressions) {
  123. if (
  124. node.parent.type === "ConditionalExpression" &&
  125. (node.parent.consequent === node || node.parent.alternate === node)
  126. ) {
  127. return isInFlaggedContext(node.parent);
  128. }
  129. /*
  130. * Check last expression only in a sequence, i.e. if ((1, 2, Boolean(3))) {}, since
  131. * the others don't affect the result of the expression.
  132. */
  133. if (
  134. node.parent.type === "SequenceExpression" &&
  135. node.parent.expressions.at(-1) === node
  136. ) {
  137. return isInFlaggedContext(node.parent);
  138. }
  139. }
  140. return isInBooleanContext(node);
  141. }
  142. /**
  143. * Check if a node has comments inside.
  144. * @param {ASTNode} node The node to check.
  145. * @returns {boolean} `true` if it has comments inside.
  146. */
  147. function hasCommentsInside(node) {
  148. return Boolean(sourceCode.getCommentsInside(node).length);
  149. }
  150. /**
  151. * Checks if the given node is wrapped in grouping parentheses. Parentheses for constructs such as if() don't count.
  152. * @param {ASTNode} node The node to check.
  153. * @returns {boolean} `true` if the node is parenthesized.
  154. * @private
  155. */
  156. function isParenthesized(node) {
  157. return eslintUtils.isParenthesized(1, node, sourceCode);
  158. }
  159. /**
  160. * Determines whether the given node needs to be parenthesized when replacing the previous node.
  161. * It assumes that `previousNode` is the node to be reported by this rule, so it has a limited list
  162. * of possible parent node types. By the same assumption, the node's role in a particular parent is already known.
  163. * @param {ASTNode} previousNode Previous node.
  164. * @param {ASTNode} node The node to check.
  165. * @throws {Error} (Unreachable.)
  166. * @returns {boolean} `true` if the node needs to be parenthesized.
  167. */
  168. function needsParens(previousNode, node) {
  169. if (previousNode.parent.type === "ChainExpression") {
  170. return needsParens(previousNode.parent, node);
  171. }
  172. if (isParenthesized(previousNode)) {
  173. // parentheses around the previous node will stay, so there is no need for an additional pair
  174. return false;
  175. }
  176. // parent of the previous node will become parent of the replacement node
  177. const parent = previousNode.parent;
  178. switch (parent.type) {
  179. case "CallExpression":
  180. case "NewExpression":
  181. return node.type === "SequenceExpression";
  182. case "IfStatement":
  183. case "DoWhileStatement":
  184. case "WhileStatement":
  185. case "ForStatement":
  186. case "SequenceExpression":
  187. return false;
  188. case "ConditionalExpression":
  189. if (previousNode === parent.test) {
  190. return precedence(node) <= precedence(parent);
  191. }
  192. if (previousNode === parent.consequent || previousNode === parent.alternate) {
  193. return precedence(node) < precedence({ type: "AssignmentExpression" });
  194. }
  195. /* c8 ignore next */
  196. throw new Error("Ternary child must be test, consequent, or alternate.");
  197. case "UnaryExpression":
  198. return precedence(node) < precedence(parent);
  199. case "LogicalExpression":
  200. if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) {
  201. return true;
  202. }
  203. if (previousNode === parent.left) {
  204. return precedence(node) < precedence(parent);
  205. }
  206. return precedence(node) <= precedence(parent);
  207. /* c8 ignore next */
  208. default:
  209. throw new Error(`Unexpected parent type: ${parent.type}`);
  210. }
  211. }
  212. return {
  213. UnaryExpression(node) {
  214. const parent = node.parent;
  215. // Exit early if it's guaranteed not to match
  216. if (node.operator !== "!" ||
  217. parent.type !== "UnaryExpression" ||
  218. parent.operator !== "!") {
  219. return;
  220. }
  221. if (isInFlaggedContext(parent)) {
  222. context.report({
  223. node: parent,
  224. messageId: "unexpectedNegation",
  225. fix(fixer) {
  226. if (hasCommentsInside(parent)) {
  227. return null;
  228. }
  229. if (needsParens(parent, node.argument)) {
  230. return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`);
  231. }
  232. let prefix = "";
  233. const tokenBefore = sourceCode.getTokenBefore(parent);
  234. const firstReplacementToken = sourceCode.getFirstToken(node.argument);
  235. if (
  236. tokenBefore &&
  237. tokenBefore.range[1] === parent.range[0] &&
  238. !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
  239. ) {
  240. prefix = " ";
  241. }
  242. return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
  243. }
  244. });
  245. }
  246. },
  247. CallExpression(node) {
  248. if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
  249. return;
  250. }
  251. if (isInFlaggedContext(node)) {
  252. context.report({
  253. node,
  254. messageId: "unexpectedCall",
  255. fix(fixer) {
  256. const parent = node.parent;
  257. if (node.arguments.length === 0) {
  258. if (parent.type === "UnaryExpression" && parent.operator === "!") {
  259. /*
  260. * !Boolean() -> true
  261. */
  262. if (hasCommentsInside(parent)) {
  263. return null;
  264. }
  265. const replacement = "true";
  266. let prefix = "";
  267. const tokenBefore = sourceCode.getTokenBefore(parent);
  268. if (
  269. tokenBefore &&
  270. tokenBefore.range[1] === parent.range[0] &&
  271. !astUtils.canTokensBeAdjacent(tokenBefore, replacement)
  272. ) {
  273. prefix = " ";
  274. }
  275. return fixer.replaceText(parent, prefix + replacement);
  276. }
  277. /*
  278. * Boolean() -> false
  279. */
  280. if (hasCommentsInside(node)) {
  281. return null;
  282. }
  283. return fixer.replaceText(node, "false");
  284. }
  285. if (node.arguments.length === 1) {
  286. const argument = node.arguments[0];
  287. if (argument.type === "SpreadElement" || hasCommentsInside(node)) {
  288. return null;
  289. }
  290. /*
  291. * Boolean(expression) -> expression
  292. */
  293. if (needsParens(node, argument)) {
  294. return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
  295. }
  296. return fixer.replaceText(node, sourceCode.getText(argument));
  297. }
  298. // two or more arguments
  299. return null;
  300. }
  301. });
  302. }
  303. }
  304. };
  305. }
  306. };